feat: calendar enhancements — view modes, post CRUD, PDF, list
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) <noreply@anthropic.com>
This commit is contained in:
@@ -159,8 +159,8 @@
|
||||
<h1>Calendar vizual</h1>
|
||||
</div>
|
||||
<div class="cv-btn-group">
|
||||
<button class="cv-btn">⤓ Export</button>
|
||||
<button class="cv-btn">🖨 Print</button>
|
||||
<button class="cv-btn" wire:click="exportPdf">🖨 PDF programări</button>
|
||||
<button class="cv-btn" wire:click="openNewPostForm">+ Pod nou</button>
|
||||
<button class="cv-btn cv-btn-primary" wire:click="openNewForm">+ Programare nouă</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,6 +195,16 @@
|
||||
<button class="cv-btn cv-btn-icon" wire:click="shiftWeek(1)">▶</button>
|
||||
<button class="cv-btn" wire:click="setWeekToday">Astăzi</button>
|
||||
</div>
|
||||
<div class="cv-toolbar-group">
|
||||
<span class="cv-toolbar-label">Mod:</span>
|
||||
<div class="cv-view-switcher">
|
||||
<button class="{{ $viewMode === 'day' ? 'active' : '' }}" wire:click="setViewMode('day')">Zi</button>
|
||||
<button class="{{ $viewMode === 'week' ? 'active' : '' }}" wire:click="setViewMode('week')">Săpt</button>
|
||||
<button class="{{ $viewMode === 'month' ? 'active' : '' }}" wire:click="setViewMode('month')">Lună</button>
|
||||
<button class="{{ $viewMode === 'custom' ? 'active' : '' }}" wire:click="setViewMode('custom')">Custom</button>
|
||||
<button class="{{ $viewMode === 'list' ? 'active' : '' }}" wire:click="setViewMode('list')">Listă</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-toolbar-group">
|
||||
<span class="cv-toolbar-label">Grupare:</span>
|
||||
<div class="cv-view-switcher">
|
||||
@@ -202,6 +212,14 @@
|
||||
<button class="{{ $groupBy === 'master' ? 'active' : '' }}" wire:click="setGroupBy('master')">Mecanic</button>
|
||||
</div>
|
||||
</div>
|
||||
@if ($viewMode === 'custom')
|
||||
<div class="cv-toolbar-group">
|
||||
<span class="cv-toolbar-label">De la:</span>
|
||||
<input type="date" class="cv-select" wire:model.live="customStart">
|
||||
<span class="cv-toolbar-label">Până la:</span>
|
||||
<input type="date" class="cv-select" wire:model.live="customEnd">
|
||||
</div>
|
||||
@endif
|
||||
<div class="cv-toolbar-group" style="margin-left:auto;">
|
||||
<span class="cv-toolbar-label">Filtru:</span>
|
||||
<select class="cv-select" wire:change="setMasterFilter($event.target.value)">
|
||||
@@ -224,8 +242,41 @@
|
||||
Matricea <strong>{{ $groupBy === 'post' ? 'Pod' : 'Mecanic' }} × Zile</strong>. Drag-and-drop între celule pentru a reprograma. Indicator de încărcare pe fiecare celulă (ore_planificate / capacitate). Click pe celulă goală → programare rapidă. Click pe eveniment → detalii.
|
||||
</div>
|
||||
|
||||
@if ($viewMode === 'list')
|
||||
@php $list = $this->getListAppointments(); @endphp
|
||||
<div class="cv-matrix-wrap">
|
||||
@if (empty($list))
|
||||
<div style="padding:32px; text-align:center; color:#718096;">Nicio programare în perioada selectată.</div>
|
||||
@else
|
||||
<table style="width:100%; border-collapse:collapse; font-size:13px;">
|
||||
<thead>
|
||||
<tr style="background:#f7fafc;">
|
||||
@foreach (['Data', 'Ora', 'Subiect', 'Client', 'Telefon', 'Auto', 'Pod', 'Maistru', 'Status'] as $h)
|
||||
<th style="text-align:left; padding:10px 12px; font-size:11px; text-transform:uppercase; color:#4a5568; border-bottom:1px solid var(--cv-border);">{{ $h }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($list as $a)
|
||||
<tr wire:click="openEvent({{ $a['id'] }})" style="cursor:pointer; border-bottom:1px solid var(--cv-border);">
|
||||
<td style="padding:10px 12px; font-weight:500;">{{ $a['date'] }}</td>
|
||||
<td style="padding:10px 12px;">{{ $a['time'] }}</td>
|
||||
<td style="padding:10px 12px; font-weight:500;">{{ $a['title'] }}</td>
|
||||
<td style="padding:10px 12px;">{{ $a['client_name'] ?? '—' }}</td>
|
||||
<td style="padding:10px 12px; color:var(--cv-blue);">{{ $a['client_phone'] ?? '—' }}</td>
|
||||
<td style="padding:10px 12px;">{{ $a['vehicle'] }}</td>
|
||||
<td style="padding:10px 12px;">{{ $a['post_name'] }}</td>
|
||||
<td style="padding:10px 12px;">{{ $a['master_name'] }}</td>
|
||||
<td style="padding:10px 12px;"><span style="background:#EDF2F7;padding:2px 8px;border-radius:4px;font-size:11px;">{{ ucfirst($a['status']) }}</span></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="cv-matrix-wrap">
|
||||
<div class="cv-grid">
|
||||
<div class="cv-grid" style="grid-template-columns: 140px repeat({{ count($days) }}, minmax({{ $viewMode === 'month' ? '70' : '160' }}px, 1fr)); min-width: {{ 140 + count($days) * ($viewMode === 'month' ? 75 : 165) }}px;">
|
||||
<div class="cv-cell cv-header-cell" style="background:#EDF2F7;">{{ $groupBy === 'post' ? 'Pod / Zi' : 'Mecanic / Zi' }}</div>
|
||||
@foreach ($days as $day)
|
||||
<div class="cv-cell cv-header-cell {{ $day['is_today'] ? 'today' : '' }} {{ $day['is_closed'] ? 'closed' : '' }}">
|
||||
@@ -235,10 +286,10 @@
|
||||
@endforeach
|
||||
|
||||
@foreach ($rows as $row)
|
||||
<div class="cv-cell cv-row-label">
|
||||
<div class="cv-cell cv-row-label" @if ($row['kind'] === 'post' && $row['id']) wire:click="openRenamePost({{ $row['id'] }})" style="cursor:pointer;" title="Click pentru a redenumi" @endif>
|
||||
<span class="cv-row-color" style="background:{{ $row['color'] }}"></span>
|
||||
<div>
|
||||
<div>{{ $row['name'] }}</div>
|
||||
<div>{{ $row['name'] }} @if ($row['kind'] === 'post') <span style="font-size:10px; opacity:0.5;">✏</span>@endif</div>
|
||||
<div class="cv-row-meta">{{ $row['meta'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,6 +332,7 @@
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="cv-legend">
|
||||
<h3>Legendă</h3>
|
||||
@@ -396,5 +448,64 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- NEW POST QUICK-ADD MODAL --}}
|
||||
@if ($showNewPostForm)
|
||||
<div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9999; display:flex; align-items:center; justify-content:center;" wire:click="$set('showNewPostForm', false)">
|
||||
<div style="background:white; border-radius:12px; padding:20px; max-width:420px; width:92%;" wire:click.stop>
|
||||
<h2 style="font-size:18px; font-weight:600; margin-bottom:12px;">+ Pod / spațiu nou</h2>
|
||||
<p style="font-size:13px; color:#4a5568; margin-bottom:16px;">Poți adăuga un loc de muncă (cu lift sau fără — ex: curte, atelier electric).</p>
|
||||
<div class="cv-pfield">
|
||||
<label>Nume *</label>
|
||||
<input type="text" wire:model="newPost.name" placeholder="Ex: Curte 1, Pod 4, Atelier electric" autofocus>
|
||||
</div>
|
||||
<div class="cv-two-cols">
|
||||
<div class="cv-pfield">
|
||||
<label>Culoare</label>
|
||||
<input type="color" wire:model="newPost.color" style="height:40px; padding:4px;">
|
||||
</div>
|
||||
<div class="cv-pfield">
|
||||
<label>Ore/zi</label>
|
||||
<input type="number" wire:model="newPost.hours_per_day" min="1" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-pfield">
|
||||
<label>Descriere (opțional)</label>
|
||||
<input type="text" wire:model="newPost.description" placeholder="Ex: fără lift, doar diagnoză...">
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:16px;">
|
||||
<button class="cv-btn" wire:click="$set('showNewPostForm', false)" style="flex:1;">Anulează</button>
|
||||
<button class="cv-btn cv-btn-primary" wire:click="createPost" style="flex:1;">Adaugă</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- RENAME POST MODAL --}}
|
||||
@if ($renamingPostId)
|
||||
<div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9999; display:flex; align-items:center; justify-content:center;" wire:click="$set('renamingPostId', null)">
|
||||
<div style="background:white; border-radius:12px; padding:20px; max-width:420px; width:92%;" wire:click.stop>
|
||||
<h2 style="font-size:18px; font-weight:600; margin-bottom:12px;">✏ Editează pod</h2>
|
||||
<div class="cv-pfield">
|
||||
<label>Nume *</label>
|
||||
<input type="text" wire:model="renamingPostName" autofocus>
|
||||
</div>
|
||||
<div class="cv-pfield">
|
||||
<label>Mecanic implicit</label>
|
||||
<select wire:model="renamingPostMasterId">
|
||||
<option value="">— niciunul —</option>
|
||||
@foreach ($this->getMasterOptions() as $id => $name)
|
||||
<option value="{{ $id }}">{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p style="font-size:11px; color:#718096; margin-top:4px;">Pre-completat automat la programări noi pe acest pod.</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:16px;">
|
||||
<button class="cv-btn" wire:click="$set('renamingPostId', null)" style="flex:1;">Anulează</button>
|
||||
<button class="cv-btn cv-btn-primary" wire:click="saveRenamePost" style="flex:1;">Salvează</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
||||
Reference in New Issue
Block a user