Explorar o código

WIP - guias dos agendamentos de consultas

Gustavo Zanatta hai 2 días
pai
achega
4b9ebe2235

+ 50 - 3
app/Http/Controllers/AppointmentController.php

@@ -5,16 +5,22 @@ namespace App\Http\Controllers;
 use App\Http\Requests\AppointmentApproveRequest;
 use App\Http\Requests\AppointmentRequest;
 use App\Http\Resources\AppointmentResource;
+use App\Models\Appointment;
+use App\Services\AppointmentGuideService;
 use App\Services\AppointmentService;
 use App\Enums\AppointmentStatusEnum;
 use App\Enums\UserStatusEnum;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Http\Response;
 use Illuminate\Support\Facades\Auth;
 
 class AppointmentController extends Controller
 {
-    public function __construct(protected AppointmentService $service) {}
+    public function __construct(
+        protected AppointmentService $service,
+        protected AppointmentGuideService $guideService,
+    ) {}
 
     public function index(): JsonResponse
     {
@@ -102,12 +108,15 @@ class AppointmentController extends Controller
         $items = collect($paginator->items())->map(fn($a) => [
             'id'                      => $a->id,
             'order_number'            => $a->order_number,
-            'registration'            => $a->user?->registration,  
+            'registration'            => $a->user?->registration,
             'user_name'               => $a->user?->name,
-            'partner_name'            => $a->partnerAgreement?->trade_name ?? $a->partnerAgreement?->company_name,
+            'dependent_name'          => $a->userDependent?->name,
+            'is_for_dependent'        => $a->user_dependent_id !== null,
+            'partner_name'            => $a->partnerAgreement?->company_name,
             'service_name'            => $a->partnerAgreementService?->name,
             'requested_at'            => $a->requested_at?->format('d/m/Y'),
             'status'                  => $a->status?->value,
+            'can_issue_guide'         => $this->guideService->canIssue($a),
         ]);
 
         return $this->successResponse(payload: [
@@ -145,4 +154,42 @@ class AppointmentController extends Controller
         if (!$item) return $this->errorResponse(message: __('messages.not_found'), code: 404);
         return $this->successResponse(payload: new AppointmentResource($item), message: __('messages.updated'));
     }
+
+    public function guide(int $id): Response|JsonResponse
+    {
+        return $this->buildGuide($this->service->findById($id));
+    }
+
+    public function myGuide(int $id): Response|JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        if ($item && $item->user_id !== Auth::id()) {
+            return $this->errorResponse(message: __('messages.unauthorized'), code: 403);
+        }
+
+        return $this->buildGuide($item);
+    }
+
+    public function partnerGuide(int $id): Response|JsonResponse
+    {
+        $item = $this->service->findByIdForPartnerUser($id, Auth::id());
+
+        return $this->buildGuide($item);
+    }
+
+    private function buildGuide(?Appointment $appointment): Response|JsonResponse
+    {
+        if (!$appointment) {
+            return $this->errorResponse(message: __('messages.not_found'), code: 404);
+        }
+
+        if (!$this->guideService->canIssue($appointment)) {
+            return $this->errorResponse(message: __('messages.guide_unavailable'), code: 422);
+        }
+
+        return $this->guideService
+            ->pdf($appointment, Auth::id())
+            ->download($this->guideService->fileName($appointment));
+    }
 }

+ 22 - 0
app/Http/Requests/AppointmentRequest.php

@@ -4,7 +4,10 @@ namespace App\Http\Requests;
 
 use App\Enums\AppointmentStatusEnum;
 use App\Enums\PartnerAgreementTypeEnum;
+use App\Enums\UserDependentStatusEnum;
+use App\Models\Appointment;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Validation\Rule;
 
 class AppointmentRequest extends FormRequest
@@ -22,8 +25,14 @@ class AppointmentRequest extends FormRequest
                 fn ($rule) => $rule->where('partner_agreement_id', $this->input('partner_agreement_id')),
             );
 
+        $dependentExists = Rule::exists('user_dependents', 'id')
+            ->where('responsible_user_id', $this->appointmentOwnerId())
+            ->where('status', UserDependentStatusEnum::APPROVED->value)
+            ->whereNull('deleted_at');
+
         $rules = [
             'user_id'                      => 'sometimes|integer|exists:users,id',
+            'user_dependent_id'            => ['sometimes', 'nullable', 'integer', $dependentExists],
             'partner_agreement_id'         => ['sometimes', 'integer', $partnerExists],
             'partner_agreement_service_id' => ['sometimes', 'integer', $serviceExists],
             'date'                         => 'sometimes|date',
@@ -40,4 +49,17 @@ class AppointmentRequest extends FormRequest
 
         return $rules;
     }
+
+    private function appointmentOwnerId(): ?int
+    {
+        if ($this->filled('user_id')) {
+            return (int) $this->input('user_id');
+        }
+
+        if ($this->route('id')) {
+            return Appointment::find($this->route('id'))?->user_id;
+        }
+
+        return Auth::id();
+    }
 }

+ 1 - 0
app/Http/Requests/CompanySettingRequest.php

@@ -24,6 +24,7 @@ class CompanySettingRequest extends FormRequest
             'contact_email'     => 'sometimes|nullable|email|max:255',
             'contact_phone'     => 'sometimes|nullable|string|max:50',
             'contact_location'  => 'sometimes|nullable|string|max:255',
+            'guide_validity_days' => 'sometimes|integer|min:1|max:365',
         ];
     }
 }

+ 14 - 1
app/Http/Resources/AppointmentResource.php

@@ -15,15 +15,28 @@ class AppointmentResource extends JsonResource
             'order_number'                 => $this->order_number,
             'user_id'                      => $this->user_id,
             'user'                         => $this->whenLoaded('user', fn() => new UserResource($this->user)),
+            'user_dependent_id'            => $this->user_dependent_id,
+            'user_dependent'               => $this->whenLoaded('userDependent', fn() => $this->userDependent ? [
+                'id'      => $this->userDependent->id,
+                'name'    => $this->userDependent->name,
+                'kinship' => $this->userDependent->kinship,
+            ] : null),
+            'is_for_dependent'             => $this->user_dependent_id !== null,
             'partner_agreement_id'         => $this->partner_agreement_id,
             'partner_agreement'            => $this->whenLoaded('partnerAgreement', fn() => new PartnerAgreementResource($this->partnerAgreement)),
             'partner_agreement_service_id' => $this->partner_agreement_service_id,
             'partner_agreement_service'    => $this->whenLoaded('partnerAgreementService', fn() => new PartnerAgreementServiceResource($this->partnerAgreementService)),
             'date'                         => $this->date?->format('Y-m-d'),
-            'time'                         => Carbon::parse($this->time)->format('H:i'),
+            'time'                         => $this->time ? Carbon::parse($this->time)->format('H:i') : null,
             'observations'                 => $this->observations,
             'requested_at'                 => $this->requested_at?->format('Y-m-d H:i:s'),
             'status'                       => $this->status,
+            'service_price'                => $this->service_price,
+            'guide_issued_at'              => $this->guide_issued_at?->format('Y-m-d H:i:s'),
+            'guide_valid_until'            => $this->guide_valid_until?->format('Y-m-d'),
+            'can_issue_guide'              => $this->resource instanceof \App\Models\Appointment
+                                                ? app(\App\Services\AppointmentGuideService::class)->canIssue($this->resource)
+                                                : false,
             'auto_approved'                => $this->auto_approved,
             'approved_by_user_id'          => $this->approved_by_user_id,
             'approved_by_user'             => $this->whenLoaded('approvedByUser', fn() => new UserResource($this->approvedByUser)),

+ 1 - 0
app/Http/Resources/CompanySettingResource.php

@@ -32,6 +32,7 @@ class CompanySettingResource extends JsonResource
             'contact_email'          => $this->contact_email,
             'contact_phone'          => $this->contact_phone,
             'contact_location'       => $this->contact_location,
+            'guide_validity_days'    => (int) ($this->guide_validity_days ?? 30),
             'updated_at'             => $this->updated_at
                                             ? Carbon::parse($this->updated_at)->format('Y-m-d H:i:s')
                                             : null,

+ 22 - 4
app/Models/Appointment.php

@@ -16,18 +16,36 @@ class Appointment extends Model
     protected function casts(): array
     {
         return [
-            'date'          => 'date',
-            'requested_at'  => 'datetime',
-            'status'        => AppointmentStatusEnum::class,
-            'auto_approved' => 'boolean',
+            'date'              => 'date',
+            'requested_at'      => 'datetime',
+            'status'            => AppointmentStatusEnum::class,
+            'auto_approved'     => 'boolean',
+            'service_price'     => 'decimal:2',
+            'guide_issued_at'   => 'datetime',
+            'guide_valid_until' => 'date',
         ];
     }
 
+    public function isForDependent(): bool
+    {
+        return $this->user_dependent_id !== null;
+    }
+
     public function user(): BelongsTo
     {
         return $this->belongsTo(User::class);
     }
 
+    public function userDependent(): BelongsTo
+    {
+        return $this->belongsTo(UserDependent::class)->withTrashed();
+    }
+
+    public function guideIssuedByUser(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'guide_issued_by_user_id');
+    }
+
     public function approvedByUser(): BelongsTo
     {
         return $this->belongsTo(User::class, 'approved_by_user_id');

+ 107 - 0
app/Services/AppointmentGuideService.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace App\Services;
+
+use App\Enums\AppointmentStatusEnum;
+use App\Models\Appointment;
+use App\Models\CompanySetting;
+use Barryvdh\DomPDF\Facade\Pdf;
+use Barryvdh\DomPDF\PDF as PdfWrapper;
+use Carbon\Carbon;
+
+class AppointmentGuideService
+{
+    private const DEFAULT_VALIDITY_DAYS = 30;
+
+    public function canIssue(Appointment $appointment): bool
+    {
+        return in_array($appointment->status, [
+                AppointmentStatusEnum::CONFIRMADO,
+                AppointmentStatusEnum::CONCLUIDO,
+            ], true)
+            && $appointment->date !== null
+            && $appointment->time !== null;
+    }
+
+    public function issue(Appointment $appointment, ?int $issuedByUserId = null): Appointment
+    {
+        if ($appointment->guide_issued_at) {
+            return $appointment;
+        }
+
+        $appointment->update([
+            'guide_issued_at'         => now(),
+            'guide_valid_until'       => now()->addDays($this->validityDays())->toDateString(),
+            'guide_issued_by_user_id' => $issuedByUserId,
+            'service_price'           => $appointment->service_price ?? $this->servicePrice($appointment),
+        ]);
+
+        return $appointment->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
+    }
+
+    public function pdf(Appointment $appointment, ?int $issuedByUserId = null): PdfWrapper
+    {
+        $appointment = $this->issue($appointment, $issuedByUserId);
+
+        return Pdf::loadView('pdf.appointment_guide', ['guide' => $this->data($appointment)])
+            ->setPaper('a4');
+    }
+
+    public function fileName(Appointment $appointment): string
+    {
+        return "guia_{$appointment->order_number}.pdf";
+    }
+
+    /**
+     * @return array<string, string|null>
+     */
+    public function data(Appointment $appointment): array
+    {
+        $price = $appointment->service_price ?? $this->servicePrice($appointment);
+
+        return [
+            'order_number'   => $appointment->order_number,
+            'holder_name'    => $appointment->user?->name,
+            'holder_badge'   => $appointment->user?->registration,
+            'dependent_name' => $appointment->userDependent?->name,
+            // O nome do médico está embutido no nome do serviço cadastrado no convênio.
+            'doctor_name'    => $appointment->partnerAgreementService?->name,
+            'clinic_name'    => $appointment->partnerAgreement?->company_name,
+            'clinic_address' => $this->clinicAddress($appointment),
+            'clinic_phone'   => $appointment->partnerAgreement?->phone,
+            'date'           => $appointment->date?->format('d/m/Y'),
+            'time'           => $appointment->time ? Carbon::parse($appointment->time)->format('H:i') : null,
+            'issued_at'      => $appointment->guide_issued_at?->format('d/m/Y'),
+            'valid_until'    => $appointment->guide_valid_until?->format('d/m/Y'),
+            'price'          => $price !== null ? 'R$ ' . number_format((float) $price, 2, ',', '.') : null,
+            'observations'   => $appointment->observations,
+        ];
+    }
+
+    private function servicePrice(Appointment $appointment): ?string
+    {
+        $service = $appointment->partnerAgreementService;
+
+        return $service?->associate_price ?? $service?->price;
+    }
+
+    private function clinicAddress(Appointment $appointment): ?string
+    {
+        $partner = $appointment->partnerAgreement;
+
+        if (!$partner) {
+            return null;
+        }
+
+        $parts = array_filter([$partner->address, $partner->neighborhood]);
+
+        return $parts ? implode(' - ', $parts) : null;
+    }
+
+    private function validityDays(): int
+    {
+        $days = (int) (CompanySetting::query()->value('guide_validity_days') ?? 0);
+
+        return $days > 0 ? $days : self::DEFAULT_VALIDITY_DAYS;
+    }
+}

+ 28 - 15
app/Services/AppointmentService.php

@@ -15,14 +15,14 @@ class AppointmentService
     public function __construct(protected NotificationService $notificationService) {}
     public function getAll(): Collection
     {
-        return Appointment::with(['user', 'partnerAgreement', 'partnerAgreementService'])
+        return Appointment::with(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService'])
             ->orderBy('date', 'desc')
             ->get();
     }
 
     public function getAllByUser(int $userId): Collection
     {
-        return Appointment::with(['partnerAgreement', 'partnerAgreementService'])
+        return Appointment::with(['userDependent', 'partnerAgreement', 'partnerAgreementService'])
             ->where('user_id', $userId)
             ->orderBy('date', 'desc')
             ->get();
@@ -30,7 +30,7 @@ class AppointmentService
 
     public function getAllByPartnerUser(int $userId): Collection
     {
-        return Appointment::with(['user', 'partnerAgreement', 'partnerAgreementService'])
+        return Appointment::with(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService'])
             ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))
             ->orderBy('date', 'desc')
             ->get();
@@ -38,7 +38,14 @@ class AppointmentService
 
     public function findById(int $id): ?Appointment
     {
-        return Appointment::with(['user', 'partnerAgreement', 'partnerAgreementService'])->find($id);
+        return Appointment::with(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService'])->find($id);
+    }
+
+    public function findByIdForPartnerUser(int $id, int $userId): ?Appointment
+    {
+        return Appointment::with(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService'])
+            ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))
+            ->find($id);
     }
 
     public function create(array $data): Appointment
@@ -46,15 +53,19 @@ class AppointmentService
         $data['order_number'] = $this->generateOrderNumber();
         $data['requested_at'] = now();
 
-        return Appointment::create($data);
+        return Appointment::create($data)
+            ->load(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     public function notifyCreation(Appointment $model): void
     {
-        $dateStr = $model->date ? Carbon::parse($model->date)->format('d/m/Y') : null;
+        $dateStr   = $model->date ? Carbon::parse($model->date)->format('d/m/Y') : null;
+        $dependent = $model->userDependent?->name;
+        $target    = $dependent ? "para o dependente {$dependent}" : 'para você';
+
         $message = $dateStr
-            ? "Um agendamento #{$model->order_number} foi criado para você" . ($model->time ? " para {$dateStr} às {$model->time}" : " em {$dateStr}") . "."
-            : "Um agendamento #{$model->order_number} foi criado para você.";
+            ? "Um agendamento #{$model->order_number} foi criado {$target}" . ($model->time ? " para {$dateStr} às {$model->time}" : " em {$dateStr}") . "."
+            : "Um agendamento #{$model->order_number} foi criado {$target}.";
 
         $this->notificationService->createAutoForUser([
             'title'     => 'Novo agendamento',
@@ -83,7 +94,7 @@ class AppointmentService
         }
 
         $model->update($data);
-        return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
+        return $model->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     public function delete(int $id): bool
@@ -108,7 +119,7 @@ class AppointmentService
 
     public function getAllPaginated(array $filters = [], int $perPage = 10): LengthAwarePaginator
     {
-        $query = Appointment::with(['user', 'partnerAgreement', 'partnerAgreementService'])
+        $query = Appointment::with(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService'])
             ->orderBy('requested_at', 'desc');
 
         if (!empty($filters['status'])) {
@@ -120,8 +131,10 @@ class AppointmentService
             $query->where(function ($q) use ($term) {
                 $q->whereHas('user', function ($uq) use ($term) {
                     $uq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
+                })->orWhereHas('userDependent', function ($dq) use ($term) {
+                    $dq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
                 })->orWhereHas('partnerAgreement', function ($pq) use ($term) {
-                    $pq->whereRaw('UNACCENT(LOWER(COALESCE(trade_name, company_name))) LIKE UNACCENT(?)', [$term]);
+                    $pq->whereRaw('UNACCENT(LOWER(company_name)) LIKE UNACCENT(?)', [$term]);
                 })->orWhereHas('partnerAgreementService', function ($sq) use ($term) {
                     $sq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
                 })->orWhereRaw('UNACCENT(LOWER(order_number)) LIKE UNACCENT(?)', [$term]);
@@ -149,7 +162,7 @@ class AppointmentService
             'source'    => 'appointment',
             'source_id' => $model->id,
         ], $model->user_id);
-        return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
+        return $model->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     public function reject(int $id): ?Appointment
@@ -164,7 +177,7 @@ class AppointmentService
             'source'    => 'appointment',
             'source_id' => $model->id,
         ], $model->user_id);
-        return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
+        return $model->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     public function approveByPartner(int $id, int $userId, string $date, string $time): ?Appointment
@@ -185,7 +198,7 @@ class AppointmentService
             'source'    => 'appointment',
             'source_id' => $model->id,
         ], $model->user_id);
-        return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
+        return $model->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     public function rejectByPartner(int $id, int $userId): ?Appointment
@@ -200,7 +213,7 @@ class AppointmentService
             'source'    => 'appointment',
             'source_id' => $model->id,
         ], $model->user_id);
-        return $model->fresh(['user', 'partnerAgreement', 'partnerAgreementService']);
+        return $model->fresh(['user', 'userDependent', 'partnerAgreement', 'partnerAgreementService']);
     }
 
     private function generateOrderNumber(): string

+ 1 - 0
composer.json

@@ -9,6 +9,7 @@
     "license": "MIT",
     "require": {
         "php": "^8.3",
+        "barryvdh/laravel-dompdf": "^3.1",
         "kalnoy/nestedset": "^6.0",
         "laravel/framework": "^12.0",
         "laravel/sanctum": "^4.0",

+ 523 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b400cab8216d79ff58ceafe39b942350",
+    "content-hash": "98d6bd95dae08a865a43fbf99f6e1a94",
     "packages": [
         {
             "name": "aws/aws-crt-php",
@@ -157,6 +157,83 @@
             },
             "time": "2026-05-21T20:14:47+00:00"
         },
+        {
+            "name": "barryvdh/laravel-dompdf",
+            "version": "v3.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/barryvdh/laravel-dompdf.git",
+                "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
+                "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
+                "shasum": ""
+            },
+            "require": {
+                "dompdf/dompdf": "^3.0",
+                "illuminate/support": "^9|^10|^11|^12|^13.0",
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "larastan/larastan": "^2.7|^3.0",
+                "orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
+                "phpro/grumphp": "^2.5",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "aliases": {
+                        "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
+                        "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
+                    },
+                    "providers": [
+                        "Barryvdh\\DomPDF\\ServiceProvider"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Barryvdh\\DomPDF\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Barry vd. Heuvel",
+                    "email": "barryvdh@gmail.com"
+                }
+            ],
+            "description": "A DOMPDF Wrapper for Laravel",
+            "keywords": [
+                "dompdf",
+                "laravel",
+                "pdf"
+            ],
+            "support": {
+                "issues": "https://github.com/barryvdh/laravel-dompdf/issues",
+                "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://fruitcake.nl",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/barryvdh",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-02-21T08:51:10+00:00"
+        },
         {
             "name": "brick/math",
             "version": "0.13.1",
@@ -685,6 +762,161 @@
             ],
             "time": "2024-02-05T11:56:58+00:00"
         },
+        {
+            "name": "dompdf/dompdf",
+            "version": "v3.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dompdf/dompdf.git",
+                "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005",
+                "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005",
+                "shasum": ""
+            },
+            "require": {
+                "dompdf/php-font-lib": "^1.0.0",
+                "dompdf/php-svg-lib": "^1.0.0",
+                "ext-dom": "*",
+                "ext-mbstring": "*",
+                "masterminds/html5": "^2.0",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "ext-gd": "*",
+                "ext-json": "*",
+                "ext-zip": "*",
+                "mockery/mockery": "^1.3",
+                "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
+                "squizlabs/php_codesniffer": "^3.5",
+                "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
+            },
+            "suggest": {
+                "ext-gd": "Needed to process images",
+                "ext-gmagick": "Improves image processing performance",
+                "ext-imagick": "Improves image processing performance",
+                "ext-zlib": "Needed for pdf stream compression"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Dompdf\\": "src/"
+                },
+                "classmap": [
+                    "lib/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1"
+            ],
+            "authors": [
+                {
+                    "name": "The Dompdf Community",
+                    "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
+                }
+            ],
+            "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
+            "homepage": "https://github.com/dompdf/dompdf",
+            "support": {
+                "issues": "https://github.com/dompdf/dompdf/issues",
+                "source": "https://github.com/dompdf/dompdf/tree/v3.1.6"
+            },
+            "time": "2026-07-20T12:29:38+00:00"
+        },
+        {
+            "name": "dompdf/php-font-lib",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dompdf/php-font-lib.git",
+                "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
+                "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "FontLib\\": "src/FontLib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "The FontLib Community",
+                    "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
+                }
+            ],
+            "description": "A library to read, parse, export and make subsets of different types of font files.",
+            "homepage": "https://github.com/dompdf/php-font-lib",
+            "support": {
+                "issues": "https://github.com/dompdf/php-font-lib/issues",
+                "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
+            },
+            "time": "2026-01-20T14:10:26+00:00"
+        },
+        {
+            "name": "dompdf/php-svg-lib",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dompdf/php-svg-lib.git",
+                "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
+                "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": "^7.1 || ^8.0",
+                "sabberworm/php-css-parser": "^8.4 || ^9.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Svg\\": "src/Svg"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-3.0-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "The SvgLib Community",
+                    "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
+                }
+            ],
+            "description": "A library to read, parse and export to PDF SVG files.",
+            "homepage": "https://github.com/dompdf/php-svg-lib",
+            "support": {
+                "issues": "https://github.com/dompdf/php-svg-lib/issues",
+                "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
+            },
+            "time": "2026-01-02T16:01:13+00:00"
+        },
         {
             "name": "dragonmantank/cron-expression",
             "version": "v3.4.0",
@@ -2822,6 +3054,73 @@
             },
             "time": "2022-12-02T22:17:43+00:00"
         },
+        {
+            "name": "masterminds/html5",
+            "version": "2.10.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Masterminds/html5-php.git",
+                "reference": "fd5018f6815fff903946d0564977b44ce8010e29"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29",
+                "reference": "fd5018f6815fff903946d0564977b44ce8010e29",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Masterminds\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Matt Butcher",
+                    "email": "technosophos@gmail.com"
+                },
+                {
+                    "name": "Matt Farina",
+                    "email": "matt@mattfarina.com"
+                },
+                {
+                    "name": "Asmir Mustafic",
+                    "email": "goetas@gmail.com"
+                }
+            ],
+            "description": "An HTML5 parser and serializer.",
+            "homepage": "http://masterminds.github.io/html5-php",
+            "keywords": [
+                "HTML5",
+                "dom",
+                "html",
+                "parser",
+                "querypath",
+                "serializer",
+                "xml"
+            ],
+            "support": {
+                "issues": "https://github.com/Masterminds/html5-php/issues",
+                "source": "https://github.com/Masterminds/html5-php/tree/2.10.1"
+            },
+            "time": "2026-06-23T18:43:15+00:00"
+        },
         {
             "name": "monolog/monolog",
             "version": "3.9.0",
@@ -4261,6 +4560,86 @@
             },
             "time": "2025-06-25T14:20:11+00:00"
         },
+        {
+            "name": "sabberworm/php-css-parser",
+            "version": "v9.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
+                "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
+                "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
+                "shasum": ""
+            },
+            "require": {
+                "ext-iconv": "*",
+                "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+                "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
+            },
+            "require-dev": {
+                "php-parallel-lint/php-parallel-lint": "1.4.0",
+                "phpstan/extension-installer": "1.4.3",
+                "phpstan/phpstan": "1.12.33 || 2.2.2",
+                "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16",
+                "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11",
+                "phpunit/phpunit": "8.5.52",
+                "rawr/phpunit-data-provider": "3.3.1",
+                "rector/rector": "1.2.10 || 2.4.6",
+                "rector/type-perfect": "1.0.0 || 2.1.3",
+                "squizlabs/php_codesniffer": "4.0.1",
+                "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3"
+            },
+            "suggest": {
+                "ext-mbstring": "for parsing UTF-8 CSS"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "9.5.x-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/Rule/Rule.php",
+                    "src/RuleSet/RuleContainer.php"
+                ],
+                "psr-4": {
+                    "Sabberworm\\CSS\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Raphael Schweikert"
+                },
+                {
+                    "name": "Oliver Klee",
+                    "email": "github@oliverklee.de"
+                },
+                {
+                    "name": "Jake Hotson",
+                    "email": "jake.github@qzdesign.co.uk"
+                }
+            ],
+            "description": "Parser for CSS Files written in PHP",
+            "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
+            "keywords": [
+                "css",
+                "parser",
+                "stylesheet"
+            ],
+            "support": {
+                "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
+                "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0"
+            },
+            "time": "2026-06-18T15:10:53+00:00"
+        },
         {
             "name": "symfony/clock",
             "version": "v7.3.0",
@@ -6566,6 +6945,149 @@
             ],
             "time": "2025-06-27T19:55:54+00:00"
         },
+        {
+            "name": "thecodingmachine/safe",
+            "version": "v3.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thecodingmachine/safe.git",
+                "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
+                "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^8.1"
+            },
+            "require-dev": {
+                "php-parallel-lint/php-parallel-lint": "^1.4",
+                "phpstan/phpstan": "^2",
+                "phpunit/phpunit": "^10",
+                "squizlabs/php_codesniffer": "^3.2"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "lib/special_cases.php",
+                    "generated/apache.php",
+                    "generated/apcu.php",
+                    "generated/array.php",
+                    "generated/bzip2.php",
+                    "generated/calendar.php",
+                    "generated/classobj.php",
+                    "generated/com.php",
+                    "generated/cubrid.php",
+                    "generated/curl.php",
+                    "generated/datetime.php",
+                    "generated/dir.php",
+                    "generated/eio.php",
+                    "generated/errorfunc.php",
+                    "generated/exec.php",
+                    "generated/fileinfo.php",
+                    "generated/filesystem.php",
+                    "generated/filter.php",
+                    "generated/fpm.php",
+                    "generated/ftp.php",
+                    "generated/funchand.php",
+                    "generated/gettext.php",
+                    "generated/gmp.php",
+                    "generated/gnupg.php",
+                    "generated/hash.php",
+                    "generated/ibase.php",
+                    "generated/ibmDb2.php",
+                    "generated/iconv.php",
+                    "generated/image.php",
+                    "generated/imap.php",
+                    "generated/info.php",
+                    "generated/inotify.php",
+                    "generated/json.php",
+                    "generated/ldap.php",
+                    "generated/libxml.php",
+                    "generated/lzf.php",
+                    "generated/mailparse.php",
+                    "generated/mbstring.php",
+                    "generated/misc.php",
+                    "generated/mysql.php",
+                    "generated/mysqli.php",
+                    "generated/network.php",
+                    "generated/oci8.php",
+                    "generated/opcache.php",
+                    "generated/openssl.php",
+                    "generated/outcontrol.php",
+                    "generated/pcntl.php",
+                    "generated/pcre.php",
+                    "generated/pgsql.php",
+                    "generated/posix.php",
+                    "generated/ps.php",
+                    "generated/pspell.php",
+                    "generated/readline.php",
+                    "generated/rnp.php",
+                    "generated/rpminfo.php",
+                    "generated/rrd.php",
+                    "generated/sem.php",
+                    "generated/session.php",
+                    "generated/shmop.php",
+                    "generated/sockets.php",
+                    "generated/sodium.php",
+                    "generated/solr.php",
+                    "generated/spl.php",
+                    "generated/sqlsrv.php",
+                    "generated/ssdeep.php",
+                    "generated/ssh2.php",
+                    "generated/stream.php",
+                    "generated/strings.php",
+                    "generated/swoole.php",
+                    "generated/uodbc.php",
+                    "generated/uopz.php",
+                    "generated/url.php",
+                    "generated/var.php",
+                    "generated/xdiff.php",
+                    "generated/xml.php",
+                    "generated/xmlrpc.php",
+                    "generated/yaml.php",
+                    "generated/yaz.php",
+                    "generated/zip.php",
+                    "generated/zlib.php"
+                ],
+                "classmap": [
+                    "lib/DateTime.php",
+                    "lib/DateTimeImmutable.php",
+                    "lib/Exceptions/",
+                    "generated/Exceptions/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
+            "support": {
+                "issues": "https://github.com/thecodingmachine/safe/issues",
+                "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/OskarStark",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/shish",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/silasjoisten",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/staabm",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-02-04T18:08:13+00:00"
+        },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
             "version": "v2.3.0",

+ 33 - 0
database/migrations/2026_08_11_000001_add_dependent_and_guide_fields_to_appointments_table.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('appointments', function (Blueprint $table) {
+            $table->foreignId('user_dependent_id')->nullable()->after('user_id')
+                ->constrained('user_dependents')->nullOnDelete();
+
+            $table->decimal('service_price', 10, 2)->nullable()->after('observations');
+            $table->timestamp('guide_issued_at')->nullable()->after('service_price');
+            $table->date('guide_valid_until')->nullable()->after('guide_issued_at');
+            $table->foreignId('guide_issued_by_user_id')->nullable()->after('guide_valid_until')
+                ->constrained('users')->nullOnDelete();
+
+            $table->index('user_dependent_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('appointments', function (Blueprint $table) {
+            $table->dropConstrainedForeignId('guide_issued_by_user_id');
+            $table->dropConstrainedForeignId('user_dependent_id');
+            $table->dropColumn(['service_price', 'guide_issued_at', 'guide_valid_until']);
+        });
+    }
+};

+ 22 - 0
database/migrations/2026_08_11_000002_add_guide_validity_days_to_company_settings_table.php

@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('company_settings', function (Blueprint $table) {
+            $table->unsignedSmallInteger('guide_validity_days')->default(30)->after('contact_location');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('company_settings', function (Blueprint $table) {
+            $table->dropColumn('guide_validity_days');
+        });
+    }
+};

+ 1 - 0
lang/en/messages.php

@@ -17,6 +17,7 @@ return [
     'appointment_created'     => 'Appointment created successfully',
     'appointment_cancelled'   => 'Appointment cancelled successfully',
     'appointment_confirmed'   => 'Appointment confirmed successfully',
+    'guide_unavailable'       => 'The guide can only be generated for confirmed appointments with a defined date and time',
     'not_found'               => 'Record not found',
     'unauthorized'            => 'Unauthorized action',
     'landing'                 => [

+ 1 - 0
lang/es/messages.php

@@ -17,6 +17,7 @@ return [
     'appointment_created'     => 'Cita creada exitosamente',
     'appointment_cancelled'   => 'Cita cancelada exitosamente',
     'appointment_confirmed'   => 'Cita confirmada exitosamente',
+    'guide_unavailable'       => 'La guía solo puede generarse para citas confirmadas con fecha y hora definidas',
     'not_found'               => 'Registro no encontrado',
     'unauthorized'            => 'Acción no autorizada',
     'landing'                 => [

+ 1 - 0
lang/pt/messages.php

@@ -17,6 +17,7 @@ return [
     'appointment_created'     => 'Agendamento criado com sucesso',
     'appointment_cancelled'   => 'Agendamento cancelado com sucesso',
     'appointment_confirmed'   => 'Agendamento confirmado com sucesso',
+    'guide_unavailable'       => 'A guia só pode ser gerada para agendamentos confirmados com data e horário definidos',
     'not_found'               => 'Registro não encontrado',
     'unauthorized'            => 'Ação não autorizada',
     'landing'                 => [

+ 165 - 0
resources/views/pdf/appointment_guide.blade.php

@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+    <meta charset="UTF-8">
+    <title>Guia de Atendimento — {{ $guide['order_number'] }}</title>
+    <style>
+        @page { margin: 24px 28px; }
+        body { font-family: DejaVu Sans, sans-serif; color: #333; font-size: 12px; margin: 0; }
+
+        .header { background-color: #4d1658; color: #ffffff; padding: 16px 20px; }
+        .header .brand { font-size: 20px; font-weight: bold; letter-spacing: 1px; }
+        .header .title { font-size: 13px; margin-top: 4px; }
+        .header .order { float: right; font-size: 13px; font-weight: bold; }
+
+        .section { border: 1px solid #d9d0dd; border-top: none; padding: 14px 20px; }
+        .section-title {
+            background-color: #f3e5f5; color: #4d1658; font-size: 11px; font-weight: bold;
+            text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 20px;
+            border: 1px solid #d9d0dd; border-top: none;
+        }
+
+        table.fields { width: 100%; border-collapse: collapse; }
+        table.fields td { padding: 5px 0; vertical-align: top; width: 50%; }
+        .label { color: #8a7f8f; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
+        .value { font-size: 13px; color: #222; }
+        .value strong { color: #4d1658; }
+
+        /* Em bloco próprio: no dompdf a tarja inline invade a linha de cima. */
+        .badge-dependent {
+            display: block; background-color: #f3e5f5; color: #4d1658;
+            border: 1px solid #c9a3dc; border-radius: 3px; padding: 3px 8px; margin-top: 6px;
+            font-size: 10px; font-weight: bold; text-transform: uppercase; width: 200px;
+        }
+
+        .price-box {
+            background-color: #f3e5f5; border: 1px solid #c9a3dc; border-radius: 4px;
+            padding: 10px 14px; text-align: right;
+        }
+        .price-box .amount { font-size: 20px; font-weight: bold; color: #4d1658; }
+
+        .footer { margin-top: 18px; color: #8a7f8f; font-size: 10px; text-align: center; line-height: 1.5; }
+        .signature { margin-top: 42px; }
+        .signature td { width: 45%; padding-top: 6px; border-top: 1px solid #999; text-align: center; font-size: 10px; color: #666; }
+        .signature .spacer { width: 10%; border: none; }
+    </style>
+</head>
+<body>
+
+<div class="header">
+    <span class="order">Nº {{ $guide['order_number'] }}</span>
+    <div class="brand">SerPrati</div>
+    <div class="title">Guia de Atendimento</div>
+</div>
+
+<div class="section-title">Beneficiário</div>
+<div class="section">
+    <table class="fields">
+        <tr>
+            <td>
+                <div class="label">Titular</div>
+                <div class="value">{{ $guide['holder_name'] ?? '—' }}</div>
+            </td>
+            <td>
+                <div class="label">Crachá</div>
+                <div class="value"><strong>{{ $guide['holder_badge'] ?? '—' }}</strong></div>
+            </td>
+        </tr>
+        @if ($guide['dependent_name'])
+            <tr>
+                <td colspan="2">
+                    <div class="label">Dependente</div>
+                    <div class="value">{{ $guide['dependent_name'] }}</div>
+                    <div class="badge-dependent">Atendimento para dependente</div>
+                </td>
+            </tr>
+        @endif
+    </table>
+</div>
+
+<div class="section-title">Atendimento</div>
+<div class="section">
+    <table class="fields">
+        <tr>
+            <td>
+                <div class="label">Médico / Serviço</div>
+                <div class="value">{{ $guide['doctor_name'] ?? '—' }}</div>
+            </td>
+            <td>
+                <div class="label">Clínica</div>
+                <div class="value">{{ $guide['clinic_name'] ?? '—' }}</div>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                <div class="label">Data da consulta</div>
+                <div class="value"><strong>{{ $guide['date'] ?? '—' }}</strong></div>
+            </td>
+            <td>
+                <div class="label">Horário</div>
+                <div class="value"><strong>{{ $guide['time'] ?? '—' }}</strong></div>
+            </td>
+        </tr>
+        @if ($guide['clinic_address'] || $guide['clinic_phone'])
+            <tr>
+                <td>
+                    <div class="label">Endereço</div>
+                    <div class="value">{{ $guide['clinic_address'] ?? '—' }}</div>
+                </td>
+                <td>
+                    <div class="label">Telefone</div>
+                    <div class="value">{{ $guide['clinic_phone'] ?? '—' }}</div>
+                </td>
+            </tr>
+        @endif
+        @if ($guide['observations'])
+            <tr>
+                <td colspan="2">
+                    <div class="label">Observações</div>
+                    <div class="value">{{ $guide['observations'] }}</div>
+                </td>
+            </tr>
+        @endif
+    </table>
+</div>
+
+<div class="section-title">Guia</div>
+<div class="section">
+    <table class="fields">
+        <tr>
+            <td>
+                <div class="label">Data de liberação</div>
+                <div class="value">{{ $guide['issued_at'] ?? '—' }}</div>
+            </td>
+            <td rowspan="2" style="vertical-align: middle;">
+                <div class="price-box">
+                    <div class="label">Valor da consulta</div>
+                    <div class="amount">{{ $guide['price'] ?? '—' }}</div>
+                </div>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                <div class="label">Válida até</div>
+                <div class="value"><strong>{{ $guide['valid_until'] ?? '—' }}</strong></div>
+            </td>
+        </tr>
+    </table>
+
+    <table class="fields signature">
+        <tr>
+            <td>Assinatura do beneficiário</td>
+            <td class="spacer"></td>
+            <td>Carimbo e assinatura da clínica</td>
+        </tr>
+    </table>
+</div>
+
+<div class="footer">
+    Apresente esta guia no atendimento junto com um documento com foto.<br>
+    Guia válida somente até {{ $guide['valid_until'] ?? '—' }} e exclusivamente para o atendimento descrito acima.<br>
+    © {{ date('Y') }} SerPrati
+</div>
+
+</body>
+</html>

+ 1 - 0
routes/authRoutes/appointment.php

@@ -13,6 +13,7 @@ Route::controller(AppointmentController::class)->prefix('appointment')->group(fu
     Route::get('/admin/counters', 'getAdminCounters')->middleware('permission:agendamento,view');
     Route::get('/admin/list', 'getAdminAppointmentsPaginated')->middleware('permission:agendamento,view');
     Route::get('/admin/user/{id}', 'getByUser')->middleware('permission:agendamento,view');
+    Route::get('/{id}/guide', 'guide')->middleware('permission:agendamento,view');
     Route::put('/{id}/approve', 'approve')->middleware('permission:agendamento,edit');
     Route::put('/{id}/reject', 'reject')->middleware('permission:agendamento,edit');
 

+ 1 - 0
routes/authRoutes/associado_appointment.php

@@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
 
 Route::controller(AppointmentController::class)->prefix('associado/appointment')->group(function () {
     Route::get('/my',    'myAppointments')->middleware('permission:associado.agendamento,view');
+    Route::get('/{id}/guide', 'myGuide')   ->middleware('permission:associado.agendamento,view');
     Route::post('/',     'store')         ->middleware('permission:associado.agendamento,add');
     Route::put('/{id}',  'update')        ->middleware('permission:associado.agendamento,edit');
 });

+ 1 - 0
routes/authRoutes/parceiro_appointment.php

@@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
 
 Route::controller(AppointmentController::class)->prefix('parceiro/appointment')->group(function () {
     Route::get('/',                'partnerAppointments')->middleware('permission:parceiro.agendamento,view');
+    Route::get('/{id}/guide',      'partnerGuide')       ->middleware('permission:parceiro.agendamento,view');
     Route::get('/{id}',            'show')               ->middleware('permission:parceiro.agendamento,view');
     Route::put('/{id}/approve',    'approveByPartner')   ->middleware('permission:parceiro.agendamento,edit');
     Route::put('/{id}/reject',     'rejectByPartner')    ->middleware('permission:parceiro.agendamento,edit');