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 @@
| {{ $h }} | + @endforeach +||||||||
|---|---|---|---|---|---|---|---|---|
| {{ $a['date'] }} | +{{ $a['time'] }} | +{{ $a['title'] }} | +{{ $a['client_name'] ?? '—' }} | +{{ $a['client_phone'] ?? '—' }} | +{{ $a['vehicle'] }} | +{{ $a['post_name'] }} | +{{ $a['master_name'] }} | +{{ ucfirst($a['status']) }} | +
Poți adăuga un loc de muncă (cu lift sau fără — ex: curte, atelier electric).
+Pre-completat automat la programări noi pe acest pod.
+| Ora | +Subiect | +Client / Auto | +Pod | +Maistru | +Status | +
|---|---|---|---|---|---|
| {{ substr($a->time_start ?? '', 0, 5) }}–{{ substr($a->time_end ?? '', 0, 5) }} | +{{ $a->title }}@if ($a->notes)@endif | +
+ {{ $a->client?->name ?? '—' }}
+
+ |
+ {{ $a->post?->name ?? '—' }} | +{{ $a->master?->name ?? '—' }} | +{{ ucfirst($a->status) }} | +