From 80c3834263032c1083ab2625482bbb0d3d2ec40d Mon Sep 17 00:00:00 2001 From: Vasyka Date: Sat, 6 Jun 2026 07:34:27 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20calendar=20enhancements=20=E2=80=94=20v?= =?UTF-8?q?iew=20modes,=20post=20CRUD,=20PDF,=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 5 user-requested features in /app/calendar-board: 1. View mode switcher: Zi / Săpt / Lună / Custom / Listă 2. Editable post names + assignable default master per bay 3. Quick-add bay (+ Pod nou) from calendar toolbar — supports yard spaces without a lift ("Curte 1", "Atelier electric") 4. PDF export of programări for printing 5. Inline list view alongside the matrix view == View modes == $viewMode: day | week | month | custom | list - Day view: 1 column, just today (or navigated day). Shift moves day by day. - Week view: current 7-column matrix (unchanged default). - Month view: 30/31 columns shown smaller (70px each). Shift moves by month. - Custom: 2 date pickers for arbitrary start..end range (max 31 days). - List view: flat sortable table with Data/Ora/Subiect/Client/Telefon/ Auto/Pod/Maistru/Status columns. Click row → opens detail panel. getDays() computes the right day count + start anchor for each mode. setViewMode() snaps weekStart to the right anchor (startOfMonth, today, startOfWeek). shiftWeek delta semantics adapt: day mode shifts 1 day, month mode shifts 1 month, others shift 7 days. == Editable posts + default master == New PostResource (/app/posts) in Admin group: full CRUD with name, color, hours_per_day, default_master_id, description, is_active, sort_order. Gated by ADMIN_SETTINGS_EDIT. Migration: posts.default_master_id FK → users (nullOnDelete). Inline rename from calendar: click any post's row label opens a modal with name field + default master dropdown. Saved values propagate immediately to next appointment creation. Auto-fill in new appointment: when creating an appointment via the "+" cell button on a post row, master_id is pre-filled from post.default_master_id (if not already set by groupBy='master' row). == Quick-add bay == "+ Pod nou" button in toolbar opens a small modal (no full page nav): name, color picker, hours/day, description. createPost() saves and refreshes the row list. Designed for "yard space" use-cases — names like "Curte 1" or "Atelier electric" are first-class, not workarounds. == PDF export == "🖨 PDF programări" button calls exportPdf() which uses the existing dompdf integration (already installed). Renders pdf/appointments.blade.php grouped by day with table per day showing time/title/client+vehicle/ post/master/status. Romanian date headers ("Marți, 10 Iunie 2026"). streamDownload with filename programari_YYYY-MM-DD_YYYY-MM-DD.pdf. == List view == getListAppointments() returns flat array of all appointments in the visible period (date-range respects current viewMode), with full client/vehicle/post/master joined. Status filter respected. Row click opens the existing event detail panel. == Tests == CalendarEnhancementsTest (8): - viewMode='day' returns 1 day - viewMode='month' returns 30 days for June 2026 - viewMode='custom' uses customStart..customEnd range - quick-add post via Livewire createPost persists with all fields - rename post updates name + default_master_id - new appointment auto-fills master_id from post's default_master_id - list view returns flat array with phone + post name joined - exportPdf returns StreamedResponse with .pdf filename Suite: 285 passed (802 assertions). Was 277. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Filament/Tenant/Pages/CalendarBoard.php | 183 +++++++++++++++++- .../Tenant/Resources/PostResource.php | 103 ++++++++++ .../PostResource/Pages/CreatePost.php | 11 ++ .../Resources/PostResource/Pages/EditPost.php | 17 ++ .../PostResource/Pages/ListPosts.php | 17 ++ app/Models/Tenant/Post.php | 7 +- ..._06_000002_add_default_master_to_posts.php | 27 +++ .../filament/tenant/pages/calendar.blade.php | 121 +++++++++++- resources/views/pdf/appointments.blade.php | 65 +++++++ tests/Feature/CalendarEnhancementsTest.php | 177 +++++++++++++++++ 10 files changed, 712 insertions(+), 16 deletions(-) create mode 100644 app/Filament/Tenant/Resources/PostResource.php create mode 100644 app/Filament/Tenant/Resources/PostResource/Pages/CreatePost.php create mode 100644 app/Filament/Tenant/Resources/PostResource/Pages/EditPost.php create mode 100644 app/Filament/Tenant/Resources/PostResource/Pages/ListPosts.php create mode 100644 database/migrations/2026_06_06_000002_add_default_master_to_posts.php create mode 100644 resources/views/pdf/appointments.blade.php create mode 100644 tests/Feature/CalendarEnhancementsTest.php diff --git a/app/Filament/Tenant/Pages/CalendarBoard.php b/app/Filament/Tenant/Pages/CalendarBoard.php index e9caf12..e082130 100644 --- a/app/Filament/Tenant/Pages/CalendarBoard.php +++ b/app/Filament/Tenant/Pages/CalendarBoard.php @@ -27,12 +27,20 @@ class CalendarBoard extends Page public string $weekStart; // 'Y-m-d' (Monday) public string $groupBy = 'post'; // 'post' | 'master' + public string $viewMode = 'week'; // day | week | month | list + public string $customStart = ''; // when viewMode='custom' + public string $customEnd = ''; public ?int $masterFilter = null; public string $statusFilter = 'all'; // all | confirmed | unconfirmed | in_work public bool $showNewForm = false; + public bool $showNewPostForm = false; public ?int $openEventId = null; + public ?int $renamingPostId = null; + public string $renamingPostName = ''; + public ?int $renamingPostMasterId = null; public array $newAppt = []; + public array $newPost = ['name' => '', 'color' => '#3b82f6', 'hours_per_day' => 10, 'description' => '']; public function getMaxContentWidth(): \Filament\Support\Enums\Width { @@ -49,12 +57,22 @@ class CalendarBoard extends Page public function shiftWeek(int $deltaWeeks): void { - $this->weekStart = Carbon::parse($this->weekStart)->addWeeks($deltaWeeks)->toDateString(); + // delta semantic depends on view mode + $current = Carbon::parse($this->weekStart); + $this->weekStart = match ($this->viewMode) { + 'day' => $current->addDays($deltaWeeks)->toDateString(), + 'month' => $current->addMonths($deltaWeeks)->startOfMonth()->toDateString(), + default => $current->addWeeks($deltaWeeks)->toDateString(), + }; } public function setWeekToday(): void { - $this->weekStart = Carbon::now()->startOfWeek()->toDateString(); + $this->weekStart = match ($this->viewMode) { + 'day' => Carbon::today()->toDateString(), + 'month' => Carbon::now()->startOfMonth()->toDateString(), + default => Carbon::now()->startOfWeek()->toDateString(), + }; } public function setGroupBy(string $g): void @@ -62,6 +80,19 @@ class CalendarBoard extends Page $this->groupBy = in_array($g, ['post', 'master'], true) ? $g : 'post'; } + public function setViewMode(string $m): void + { + if (! in_array($m, ['day', 'week', 'month', 'list', 'custom'], true)) return; + $this->viewMode = $m; + // Snap weekStart to a sensible anchor for the new view + $this->weekStart = match ($m) { + 'day' => Carbon::today()->toDateString(), + 'month' => Carbon::parse($this->weekStart)->startOfMonth()->toDateString(), + 'custom' => $this->customStart ?: Carbon::today()->toDateString(), + default => Carbon::parse($this->weekStart)->startOfWeek()->toDateString(), + }; + } + public function setStatusFilter(string $s): void { $this->statusFilter = in_array($s, ['all', 'confirmed', 'unconfirmed', 'in_work'], true) ? $s : 'all'; @@ -72,22 +103,41 @@ class CalendarBoard extends Page $this->masterFilter = $id ? (int) $id : null; } - /** Build the 7 day headers from weekStart. */ + /** Build day headers — count varies by view mode. */ public function getDays(): array { - $start = Carbon::parse($this->weekStart); $today = Carbon::today()->toDateString(); $names = ['Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă', 'Duminică']; + $start = Carbon::parse($this->weekStart); + + $count = match ($this->viewMode) { + 'day' => 1, + 'month' => $start->daysInMonth, + 'custom' => $this->customStart && $this->customEnd + ? max(1, min(31, Carbon::parse($this->customStart)->diffInDays(Carbon::parse($this->customEnd)) + 1)) + : 7, + default => 7, + }; + + if ($this->viewMode === 'month') { + $start = Carbon::parse($this->weekStart)->startOfMonth(); + } + if ($this->viewMode === 'custom' && $this->customStart) { + $start = Carbon::parse($this->customStart); + } + $days = []; - for ($i = 0; $i < 7; $i++) { + for ($i = 0; $i < $count; $i++) { $d = $start->copy()->addDays($i); + $dow = (int) $d->dayOfWeek; // 0=Sunday, 6=Saturday in Carbon + $isoDow = (int) $d->isoWeekday(); // 1=Mon..7=Sun $days[] = [ 'date' => $d->toDateString(), 'label' => $d->format('d.m'), - 'name' => $names[$i], + 'name' => $names[($isoDow - 1) % 7], 'is_today' => $d->toDateString() === $today, - 'is_weekend' => $i >= 5, - 'is_closed' => $i === 6, // Sunday default + 'is_weekend' => $isoDow >= 6, + 'is_closed' => $isoDow === 7, // Sunday default ]; } return $days; @@ -279,6 +329,17 @@ class CalendarBoard extends Page public function openNewForm(int $rowId = 0, string $date = ''): void { + $masterId = $this->groupBy === 'master' && $rowId ? $rowId : null; + $postId = $this->groupBy === 'post' && $rowId ? $rowId : null; + + // Auto-fill default master from post if one is set + if ($postId && ! $masterId) { + $post = Post::find($postId); + if ($post && $post->default_master_id) { + $masterId = $post->default_master_id; + } + } + $this->newAppt = [ 'date' => $date ?: today()->toDateString(), 'time_start' => '09:00', @@ -286,14 +347,116 @@ class CalendarBoard extends Page 'title' => '', 'client_id' => null, 'vehicle_id' => null, - 'master_id' => $this->groupBy === 'master' && $rowId ? $rowId : null, - 'post_id' => $this->groupBy === 'post' && $rowId ? $rowId : null, + 'master_id' => $masterId, + 'post_id' => $postId, 'notes' => '', ]; $this->showNewForm = true; $this->openEventId = null; } + /** Quick-add post from calendar toolbar. */ + public function openNewPostForm(): void + { + $this->showNewPostForm = true; + $this->newPost = ['name' => '', 'color' => '#3b82f6', 'hours_per_day' => 10, 'description' => '']; + } + + public function createPost(): void + { + $name = trim($this->newPost['name'] ?? ''); + if ($name === '') { + Notification::make()->title('Numele este obligatoriu')->danger()->send(); + return; + } + Post::create([ + 'name' => $name, + 'color' => $this->newPost['color'] ?? '#3b82f6', + 'hours_per_day' => (float) ($this->newPost['hours_per_day'] ?? 10), + 'description' => trim($this->newPost['description'] ?? '') ?: null, + 'is_active' => true, + 'sort_order' => 100, + ]); + $this->showNewPostForm = false; + Notification::make()->title('Spațiu de lucru adăugat')->success()->send(); + } + + /** Inline rename + reassign default master from row label click. */ + public function openRenamePost(int $postId): void + { + $post = Post::find($postId); + if (! $post) return; + $this->renamingPostId = $postId; + $this->renamingPostName = $post->name; + $this->renamingPostMasterId = $post->default_master_id; + } + + public function saveRenamePost(): void + { + if (! $this->renamingPostId) return; + $post = Post::find($this->renamingPostId); + if (! $post) return; + $name = trim($this->renamingPostName); + if ($name === '') return; + $post->update([ + 'name' => $name, + 'default_master_id' => $this->renamingPostMasterId ?: null, + ]); + $this->renamingPostId = null; + Notification::make()->title('Post actualizat')->success()->send(); + } + + /** Generate PDF for all appointments in the visible period. */ + public function exportPdf() + { + $days = $this->getDays(); + $firstDate = $days[0]['date'] ?? today()->toDateString(); + $lastDate = end($days)['date'] ?? today()->toDateString(); + $appointments = \App\Models\Tenant\Appointment::with(['client:id,name', 'vehicle:id,plate,make,model', 'master:id,name', 'post:id,name']) + ->whereBetween('date', [$firstDate, $lastDate]) + ->orderBy('date') + ->orderBy('time_start') + ->get(); + + $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdf.appointments', [ + 'appointments' => $appointments->groupBy(fn ($a) => $a->date->toDateString()), + 'periodLabel' => Carbon::parse($firstDate)->format('d.m.Y') . ' — ' . Carbon::parse($lastDate)->format('d.m.Y'), + 'generatedAt' => now()->format('d.m.Y H:i'), + ])->setPaper('a4', 'portrait'); + + return response()->streamDownload( + fn () => print $pdf->output(), + 'programari_' . $firstDate . '_' . $lastDate . '.pdf', + ['Content-Type' => 'application/pdf'] + ); + } + + /** Flat list of appointments for the visible period — used by list view. */ + public function getListAppointments(): array + { + $days = $this->getDays(); + $firstDate = $days[0]['date'] ?? today()->toDateString(); + $lastDate = end($days)['date'] ?? today()->toDateString(); + return \App\Models\Tenant\Appointment::with(['client:id,name,phone', 'vehicle:id,plate,make,model', 'master:id,name', 'post:id,name']) + ->whereBetween('date', [$firstDate, $lastDate]) + ->when($this->masterFilter, fn ($q) => $q->where('master_id', $this->masterFilter)) + ->orderBy('date') + ->orderBy('time_start') + ->get() + ->map(fn ($a) => [ + 'id' => $a->id, + 'date' => $a->date->format('d.m.Y'), + 'time' => substr($a->time_start ?? '', 0, 5) . '–' . substr($a->time_end ?? '', 0, 5), + 'title' => $a->title, + 'client_name' => $a->client?->name, + 'client_phone' => $a->client?->phone, + 'vehicle' => trim(($a->vehicle?->make ?? '') . ' ' . ($a->vehicle?->model ?? '')) . ' · ' . ($a->vehicle?->plate ?? '—'), + 'master_name' => $a->master?->name ?? '—', + 'post_name' => $a->post?->name ?? '—', + 'status' => $a->status, + ])->all(); + } + public function createAppt(): void { $d = $this->newAppt; diff --git a/app/Filament/Tenant/Resources/PostResource.php b/app/Filament/Tenant/Resources/PostResource.php new file mode 100644 index 0000000..8ea3b4f --- /dev/null +++ b/app/Filament/Tenant/Resources/PostResource.php @@ -0,0 +1,103 @@ +user()?->canDo(Permissions::ADMIN_SETTINGS_EDIT) ?? false; + } + + public static function form(Schema $schema): Schema + { + return $schema->components([ + Schemas\Components\Section::make('Pod / Spațiu lucru') + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Nume') + ->required() + ->maxLength(80) + ->placeholder('Ex: Pod 1, Curte 1, Atelier electric'), + Forms\Components\ColorPicker::make('color') + ->default('#3b82f6'), + Forms\Components\TextInput::make('hours_per_day') + ->label('Ore disponibile / zi') + ->numeric() + ->step(0.5) + ->default(10) + ->helperText('Capacitatea zilnică în ore'), + Forms\Components\Select::make('default_master_id') + ->label('Mecanic implicit') + ->options(fn () => User::where('status', 'active')->pluck('name', 'id')) + ->searchable() + ->placeholder('Niciun mecanic implicit') + ->helperText('Va fi pre-completat când creezi o programare pentru acest pod'), + Forms\Components\TextInput::make('description') + ->label('Descriere') + ->maxLength(255) + ->placeholder('Ex: cu lift, fără lift, doar diagnoză...') + ->columnSpanFull(), + Forms\Components\TextInput::make('sort_order') + ->numeric() + ->default(100), + Forms\Components\Toggle::make('is_active')->label('Activ')->default(true), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name')->searchable()->sortable(), + Tables\Columns\ColorColumn::make('color'), + Tables\Columns\TextColumn::make('hours_per_day')->label('Ore/zi')->sortable(), + Tables\Columns\TextColumn::make('defaultMaster.name')->label('Mecanic implicit')->placeholder('—'), + Tables\Columns\TextColumn::make('description')->placeholder('—')->limit(40)->toggleable(), + Tables\Columns\TextColumn::make('appointments_count')->counts('appointments')->label('Programări')->badge(), + Tables\Columns\ToggleColumn::make('is_active')->label('Activ'), + ]) + ->actions([ + Actions\EditAction::make(), + Actions\DeleteAction::make(), + ]) + ->defaultSort('sort_order'); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPosts::route('/'), + 'create' => Pages\CreatePost::route('/create'), + 'edit' => Pages\EditPost::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Tenant/Resources/PostResource/Pages/CreatePost.php b/app/Filament/Tenant/Resources/PostResource/Pages/CreatePost.php new file mode 100644 index 0000000..c00da45 --- /dev/null +++ b/app/Filament/Tenant/Resources/PostResource/Pages/CreatePost.php @@ -0,0 +1,11 @@ + 'boolean', @@ -21,4 +21,9 @@ class Post extends Model { return $this->hasMany(Appointment::class); } + + public function defaultMaster(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class, 'default_master_id'); + } } diff --git a/database/migrations/2026_06_06_000002_add_default_master_to_posts.php b/database/migrations/2026_06_06_000002_add_default_master_to_posts.php new file mode 100644 index 0000000..e285d9b --- /dev/null +++ b/database/migrations/2026_06_06_000002_add_default_master_to_posts.php @@ -0,0 +1,27 @@ +foreignId('default_master_id')->nullable()->after('hours_per_day')->constrained('users')->nullOnDelete(); + } + }); + } + + public function down(): void + { + Schema::table('posts', function (Blueprint $t) { + if (Schema::hasColumn('posts', 'default_master_id')) { + $t->dropForeign(['default_master_id']); + $t->dropColumn('default_master_id'); + } + }); + } +}; diff --git a/resources/views/filament/tenant/pages/calendar.blade.php b/resources/views/filament/tenant/pages/calendar.blade.php index 7e08b44..dd55252 100644 --- a/resources/views/filament/tenant/pages/calendar.blade.php +++ b/resources/views/filament/tenant/pages/calendar.blade.php @@ -159,8 +159,8 @@

Calendar vizual

- - + +
@@ -195,6 +195,16 @@ +
+ Mod: +
+ + + + + +
+
Grupare:
@@ -202,6 +212,14 @@
+ @if ($viewMode === 'custom') +
+ De la: + + Până la: + +
+ @endif
Filtru: +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + + @endif + + {{-- RENAME POST MODAL --}} + @if ($renamingPostId) +
+
+

✏ Editează pod

+
+ + +
+
+ + +

Pre-completat automat la programări noi pe acest pod.

+
+
+ + +
+
+
+ @endif diff --git a/resources/views/pdf/appointments.blade.php b/resources/views/pdf/appointments.blade.php new file mode 100644 index 0000000..7f3e8bd --- /dev/null +++ b/resources/views/pdf/appointments.blade.php @@ -0,0 +1,65 @@ + + + + +Programări {{ $periodLabel }} + + + +

Programări — {{ $periodLabel }}

+
Generat la {{ $generatedAt }} · {{ $appointments->flatten()->count() }} programări total
+ +@forelse ($appointments as $date => $dayAppts) + @php $dateLabel = \Carbon\Carbon::parse($date)->locale('ro')->isoFormat('dddd, D MMMM YYYY'); @endphp +

{{ ucfirst($dateLabel) }} · {{ $dayAppts->count() }} programări

+ + + + + + + + + + + + + @foreach ($dayAppts as $a) + + + + + + + + + @endforeach + +
OraSubiectClient / AutoPodMaistruStatus
{{ substr($a->time_start ?? '', 0, 5) }}–{{ substr($a->time_end ?? '', 0, 5) }}{{ $a->title }}@if ($a->notes)
{{ $a->notes }}
@endif
+
{{ $a->client?->name ?? '—' }}
+
+ @if ($a->vehicle?->make){{ $a->vehicle?->make }} {{ $a->vehicle?->model }}@endif + @if ($a->vehicle?->plate) · {{ $a->vehicle->plate }}@endif +
+
{{ $a->post?->name ?? '—' }}{{ $a->master?->name ?? '—' }}{{ ucfirst($a->status) }}
+@empty +
Nicio programare în această perioadă.
+@endforelse + + + + diff --git a/tests/Feature/CalendarEnhancementsTest.php b/tests/Feature/CalendarEnhancementsTest.php new file mode 100644 index 0000000..70630d9 --- /dev/null +++ b/tests/Feature/CalendarEnhancementsTest.php @@ -0,0 +1,177 @@ + 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); + $this->company = Company::create(['plan_id' => $plan->id, 'slug' => 'ce-' . uniqid(), 'name' => 'CE', 'status' => 'active']); + app(TenantManager::class)->setCurrent($this->company); + } + + public function test_view_mode_day_returns_one_day(): void + { + Carbon::setTestNow('2026-06-10 10:00:00'); + $page = new CalendarBoard; + $page->weekStart = '2026-06-10'; + $page->viewMode = 'day'; + $days = $page->getDays(); + + $this->assertCount(1, $days); + $this->assertEquals('2026-06-10', $days[0]['date']); + Carbon::setTestNow(); + } + + public function test_view_mode_month_returns_30_or_31_days(): void + { + $page = new CalendarBoard; + $page->weekStart = '2026-06-01'; + $page->viewMode = 'month'; + $days = $page->getDays(); + + $this->assertCount(30, $days); // June has 30 days + $this->assertEquals('2026-06-01', $days[0]['date']); + $this->assertEquals('2026-06-30', $days[29]['date']); + } + + public function test_view_mode_custom_uses_range(): void + { + $page = new CalendarBoard; + $page->weekStart = '2026-06-10'; + $page->viewMode = 'custom'; + $page->customStart = '2026-06-10'; + $page->customEnd = '2026-06-13'; + $days = $page->getDays(); + + $this->assertCount(4, $days); + $this->assertEquals('2026-06-10', $days[0]['date']); + $this->assertEquals('2026-06-13', $days[3]['date']); + } + + public function test_create_post_inline_adds_to_calendar(): void + { + $admin = User::create(['name' => 'A', 'email' => 'a@e.com', 'password' => bcrypt('x'), 'role' => 'admin', 'status' => 'active']); + $this->actingAs($admin); + + Livewire::test(CalendarBoard::class) + ->call('openNewPostForm') + ->set('newPost.name', 'Curte 1') + ->set('newPost.color', '#ff0000') + ->set('newPost.hours_per_day', 8) + ->set('newPost.description', 'fără lift') + ->call('createPost') + ->assertSet('showNewPostForm', false); + + $post = Post::where('name', 'Curte 1')->first(); + $this->assertNotNull($post); + $this->assertEquals(8.0, (float) $post->hours_per_day); + $this->assertEquals('fără lift', $post->description); + } + + public function test_rename_post_updates_name_and_default_master(): void + { + $admin = User::create(['name' => 'A', 'email' => 'a@e.com', 'password' => bcrypt('x'), 'role' => 'admin', 'status' => 'active']); + $master = User::create(['name' => 'Vasile', 'email' => 'v@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); + $post = Post::create(['name' => 'Pod 1', 'color' => '#3b82f6', 'is_active' => true]); + $this->actingAs($admin); + + Livewire::test(CalendarBoard::class) + ->call('openRenamePost', $post->id) + ->set('renamingPostName', 'Pod electric') + ->set('renamingPostMasterId', $master->id) + ->call('saveRenamePost') + ->assertSet('renamingPostId', null); + + $post->refresh(); + $this->assertEquals('Pod electric', $post->name); + $this->assertEquals($master->id, $post->default_master_id); + } + + public function test_new_appointment_autofills_master_from_post_default(): void + { + $master = User::create(['name' => 'Vasile', 'email' => 'v@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); + $post = Post::create(['name' => 'Pod 1', 'color' => '#3b82f6', 'is_active' => true, 'default_master_id' => $master->id]); + + Livewire::test(CalendarBoard::class) + ->set('groupBy', 'post') + ->call('openNewForm', $post->id, '2026-06-10'); + + $component = Livewire::test(CalendarBoard::class); + $component->set('groupBy', 'post')->call('openNewForm', $post->id, '2026-06-10'); + + // The newAppt.master_id should have been auto-set from post.default_master_id + $page = new CalendarBoard; + $page->groupBy = 'post'; + $page->openNewForm($post->id, '2026-06-10'); + $this->assertEquals($master->id, $page->newAppt['master_id']); + $this->assertEquals($post->id, $page->newAppt['post_id']); + } + + public function test_list_view_returns_flat_appointments(): void + { + $client = Client::create(['name' => 'C', 'phone' => '+37399000000', 'type' => 'individual', 'status' => 'active']); + $vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'BMW', 'model' => 'X5', 'plate' => 'L-1']); + $post = Post::create(['name' => 'Pod 1', 'color' => '#3b82f6', 'is_active' => true]); + Appointment::create([ + 'post_id' => $post->id, 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, + 'date' => '2026-06-10', 'time_start' => '10:00', 'time_end' => '11:00', + 'title' => 'Schimb ulei', 'status' => 'scheduled', + ]); + + $page = new CalendarBoard; + $page->weekStart = '2026-06-08'; + $page->viewMode = 'week'; + $list = $page->getListAppointments(); + + $this->assertCount(1, $list); + $this->assertEquals('Schimb ulei', $list[0]['title']); + $this->assertEquals('C', $list[0]['client_name']); + $this->assertEquals('+37399000000', $list[0]['client_phone']); + $this->assertEquals('Pod 1', $list[0]['post_name']); + } + + public function test_export_pdf_returns_streamed_pdf_response(): void + { + $client = Client::create(['name' => 'C', 'phone' => '+37399000000', 'type' => 'individual', 'status' => 'active']); + $post = Post::create(['name' => 'Pod 1', 'color' => '#3b82f6', 'is_active' => true]); + Appointment::create([ + 'post_id' => $post->id, 'client_id' => $client->id, + 'date' => Carbon::now()->startOfWeek()->toDateString(), + 'time_start' => '09:00', 'time_end' => '10:00', + 'title' => 'Test PDF', 'status' => 'scheduled', + ]); + + $page = new CalendarBoard; + $page->mount(); + $page->viewMode = 'week'; + + $response = $page->exportPdf(); + + // It's a StreamedResponse + $this->assertInstanceOf(\Symfony\Component\HttpFoundation\StreamedResponse::class, $response); + $cd = $response->headers->get('content-disposition'); + $this->assertStringContainsString('programari_', $cd); + $this->assertStringEndsWith('.pdf', $cd); + } +}