Files
Vasyka 80c3834263 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>
2026-06-06 07:34:27 +00:00

512 lines
32 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-filament-panels::page>
@php
$days = $this->getDays();
$rows = $this->getRows();
$matrix = $this->getMatrix();
$stats = $this->getStats();
$openEvent = $this->getOpenEvent();
@endphp
<style>
:root {
--cv-bg: #F5F7FA;
--cv-surface: #FFFFFF;
--cv-border: #E2E8F0;
--cv-text: #1A202C;
--cv-text-2: #4A5568;
--cv-text-3: #718096;
--cv-blue: #3B82F6;
--cv-blue-bg: #EBF5FF;
--cv-blue-text: #2563EB;
--cv-green: #10B981;
--cv-green-text: #059669;
--cv-orange: #D97706;
--cv-orange-bg: #FED7AA;
--cv-orange-text: #9A3412;
--cv-red: #DC2626;
--cv-red-bg: #FECACA;
--cv-red-text: #991B1B;
--cv-row-bg: #F7FAFC;
--cv-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.dark {
--cv-bg: #0f172a;
--cv-surface: #1f2937;
--cv-border: #374151;
--cv-text: #f1f5f9;
--cv-text-2: #cbd5e1;
--cv-text-3: #94a3b8;
--cv-row-bg: #111827;
--cv-blue-bg: #1e293b;
--cv-orange-bg: #422006;
--cv-red-bg: #450a0a;
}
.fi-main-ctn:has(.cv-shell) { padding: 0 !important; }
.fi-main:has(.cv-shell) { padding: 0 !important; }
.fi-page:has(.cv-shell) > div { padding: 0 !important; gap: 0 !important; }
.fi-page:has(.cv-shell) .fi-header { display: none !important; }
.cv-shell { background:var(--cv-bg); color:var(--cv-text); padding:24px; min-height:calc(100vh - 64px); font-size:13px; }
.cv-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; }
.cv-head h1 { font-size:24px; font-weight:700; margin:0; }
.cv-breadcrumb { font-size:13px; color:var(--cv-text-3); margin-bottom:6px; }
.cv-btn { padding:8px 14px; border:1px solid var(--cv-border); border-radius:6px; background:var(--cv-surface); cursor:pointer; font-size:14px; color:var(--cv-text); text-decoration:none; }
.cv-btn:hover { background:var(--cv-row-bg); }
.cv-btn-primary { background:var(--cv-blue); color:#fff !important; border-color:var(--cv-blue); }
.cv-btn-primary:hover { background:#2563EB; }
.cv-btn-icon { padding:8px 10px; }
.cv-btn-group { display:flex; gap:8px; }
/* KPI bar */
.cv-kpi-bar { display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; margin-bottom:16px; }
.cv-kpi { background:var(--cv-surface); padding:14px 16px; border-radius:8px; box-shadow:var(--cv-shadow); }
.cv-kpi-label { font-size:11px; color:var(--cv-text-3); text-transform:uppercase; letter-spacing:.5px; font-weight:600; }
.cv-kpi-value { font-size:22px; font-weight:700; margin-top:4px; }
.cv-kpi-sub { font-size:12px; color:var(--cv-text-3); margin-top:2px; }
.cv-kpi-value.green { color:var(--cv-green-text); }
.cv-kpi-value.orange { color:var(--cv-orange); }
.cv-kpi-value.red { color:var(--cv-red); }
/* Toolbar */
.cv-toolbar { display:flex; gap:12px; align-items:center; background:var(--cv-surface); padding:12px 16px; border-radius:8px; box-shadow:var(--cv-shadow); margin-bottom:16px; flex-wrap:wrap; }
.cv-toolbar-group { display:flex; gap:8px; align-items:center; }
.cv-toolbar-label { font-size:12px; color:var(--cv-text-3); font-weight:500; text-transform:uppercase; letter-spacing:.5px; }
.cv-date { font-size:16px; font-weight:600; min-width:220px; text-align:center; }
.cv-view-switcher { display:inline-flex; background:#EDF2F7; padding:3px; border-radius:6px; }
.dark .cv-view-switcher { background:#111827; }
.cv-view-switcher button { padding:6px 14px; border:none; background:transparent; cursor:pointer; border-radius:4px; font-size:13px; font-weight:500; color:var(--cv-text-2); }
.cv-view-switcher button.active { background:var(--cv-surface); color:var(--cv-text); box-shadow:0 1px 2px rgba(0,0,0,0.1); }
.cv-select { padding:8px 12px; border:1px solid var(--cv-border); border-radius:6px; background:var(--cv-surface); font-size:14px; color:var(--cv-text); }
/* Notes */
.cv-notes { background:#FFFBEB; border:1px solid #FCD34D; padding:12px 14px; border-radius:6px; font-size:13px; color:#78350F; line-height:1.5; margin-bottom:16px; }
.dark .cv-notes { background:#422006; color:#fde68a; border-color:#854d0e; }
.cv-notes strong { display:block; margin-bottom:4px; }
/* Matrix */
.cv-matrix-wrap { background:var(--cv-surface); border-radius:8px; padding:16px; box-shadow:var(--cv-shadow); overflow-x:auto; }
.cv-grid { display:grid; grid-template-columns:140px repeat(7, minmax(160px, 1fr)); gap:1px; background:var(--cv-border); border-radius:6px; overflow:hidden; min-width:1240px; }
.cv-cell { background:var(--cv-surface); padding:10px 8px; min-height:90px; position:relative; }
.cv-header-cell { background:var(--cv-row-bg); text-align:center; font-weight:600; font-size:13px; padding:10px; }
.cv-day-name { display:block; font-size:11px; text-transform:uppercase; color:var(--cv-text-3); letter-spacing:.5px; }
.cv-day-num { display:block; font-size:18px; margin-top:2px; }
.cv-header-cell.today { background:var(--cv-blue-bg); }
.cv-header-cell.today .cv-day-num { color:var(--cv-blue-text); }
.cv-header-cell.closed { opacity:0.6; }
.cv-row-label { background:var(--cv-row-bg); display:flex; align-items:center; gap:8px; font-weight:600; font-size:14px; padding:10px 12px; }
.cv-row-color { width:12px; height:12px; border-radius:50%; flex-shrink:0; }
.cv-row-meta { font-size:11px; color:var(--cv-text-3); font-weight:400; margin-top:2px; }
.cv-cell-body { cursor:pointer; }
.cv-cell-body:hover { background:var(--cv-row-bg); }
.cv-cell-body.weekend { background:#FAFAFA; }
.dark .cv-cell-body.weekend { background:#0a0f1c; }
.cv-cell-body.closed { background:repeating-linear-gradient(45deg, #FAFAFA, #FAFAFA 4px, #F1F5F9 4px, #F1F5F9 8px); cursor:not-allowed; }
.dark .cv-cell-body.closed { background:repeating-linear-gradient(45deg, #0a0f1c, #0a0f1c 4px, #111827 4px, #111827 8px); }
.cv-cell.over { background:var(--cv-blue-bg) !important; }
.cv-load { position:absolute; top:4px; right:6px; font-size:10px; background:rgba(255,255,255,0.92); padding:1px 6px; border-radius:8px; color:var(--cv-text-2); font-weight:600; }
.dark .cv-load { background:rgba(0,0,0,0.5); color:var(--cv-text-2); }
.cv-load.warn { background:var(--cv-orange-bg); color:var(--cv-orange-text); }
.cv-load.full { background:var(--cv-red-bg); color:var(--cv-red-text); }
.cv-event { background:#EBF5FF; border-left:3px solid var(--cv-blue); padding:6px 8px; margin-bottom:4px; border-radius:4px; cursor:grab; font-size:12px; transition:all .15s; }
.cv-event:hover { transform:translateY(-1px); box-shadow:0 2px 6px rgba(0,0,0,0.08); }
.cv-event:active { cursor:grabbing; }
.cv-event.dragging { opacity:0.4; }
.cv-event-title { font-weight:600; color:var(--cv-blue-text); }
.cv-event-meta { color:var(--cv-text-2); font-size:11px; margin-top:2px; }
.cv-event-time { font-weight:500; color:var(--cv-text); font-size:11px; margin-top:2px; }
.cv-add { position:absolute; bottom:4px; right:4px; width:24px; height:24px; border-radius:50%; background:var(--cv-blue); color:white; border:none; cursor:pointer; font-size:16px; line-height:22px; opacity:0; transition:opacity .15s; box-shadow:0 2px 4px rgba(0,0,0,0.15); }
.cv-cell:hover .cv-add { opacity:1; }
.cv-legend { background:var(--cv-surface); border-radius:8px; padding:16px; box-shadow:var(--cv-shadow); margin-top:16px; }
.cv-legend h3 { font-size:14px; margin-bottom:12px; color:var(--cv-text); }
.cv-legend-row { display:flex; gap:32px; flex-wrap:wrap; align-items:flex-start; }
.cv-legend-col { font-size:11px; color:var(--cv-text-3); text-transform:uppercase; margin-bottom:8px; font-weight:600; }
.cv-legend-items { display:flex; gap:16px; flex-wrap:wrap; font-size:12px; color:var(--cv-text-2); align-items:center; }
.cv-legend-item { display:inline-flex; gap:6px; align-items:center; }
.cv-legend-dot { width:12px; height:12px; border-radius:3px; }
/* Side panel */
.cv-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.25); z-index:100; opacity:0; pointer-events:none; transition:opacity .2s; }
.cv-overlay.open { opacity:1; pointer-events:all; }
.cv-panel { position:fixed; right:0; top:0; bottom:0; width:440px; max-width:92vw; background:var(--cv-surface); border-left:1px solid var(--cv-border); z-index:101; transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1); display:flex; flex-direction:column; }
.cv-panel.open { transform:translateX(0); }
.cv-panel-head { padding:16px 20px; border-bottom:1px solid var(--cv-border); display:flex; align-items:flex-start; justify-content:space-between; }
.cv-panel-title { font-size:15px; font-weight:600; }
.cv-panel-sub { font-size:11px; color:var(--cv-text-3); margin-top:2px; }
.cv-close { width:28px; height:28px; border-radius:6px; border:1px solid var(--cv-border); background:var(--cv-row-bg); cursor:pointer; display:flex; align-items:center; justify-content:center; }
.cv-panel-body { padding:16px 20px; flex:1; overflow-y:auto; }
.cv-pfield { margin-bottom:12px; }
.cv-pfield label { display:block; font-size:10px; font-weight:600; color:var(--cv-text-3); text-transform:uppercase; letter-spacing:.5px; margin-bottom:4px; }
.cv-pfield input, .cv-pfield textarea, .cv-pfield select { width:100%; padding:8px 10px; font-size:13px; border:1px solid var(--cv-border); border-radius:6px; background:var(--cv-surface); color:var(--cv-text); outline:none; font-family:inherit; }
.cv-pfield input:focus { border-color:var(--cv-blue); }
.cv-pfield-val { font-size:13px; color:var(--cv-text); }
.cv-panel-actions { padding:12px 20px; border-top:1px solid var(--cv-border); display:flex; gap:8px; }
.cv-panel-actions .cv-btn { flex:1; text-align:center; }
.cv-two-cols { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
</style>
<div class="cv-shell" x-data="{ dragEventId: null }">
<div class="cv-head">
<div>
<div class="cv-breadcrumb">CRM Programări <strong style="color:var(--cv-text);">Calendar vizual</strong></div>
<h1>Calendar vizual</h1>
</div>
<div class="cv-btn-group">
<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>
<div class="cv-kpi-bar">
<div class="cv-kpi">
<div class="cv-kpi-label">Ore programate</div>
<div class="cv-kpi-value">{{ $stats['scheduled_hours'] }} / {{ $stats['capacity_hours'] }}</div>
<div class="cv-kpi-sub">săptămâna curentă · {{ $stats['utilization_pct'] }}% capacitate</div>
</div>
<div class="cv-kpi">
<div class="cv-kpi-label">Fișe deschise</div>
<div class="cv-kpi-value orange">{{ $stats['open_count'] }}</div>
<div class="cv-kpi-sub">programări active</div>
</div>
<div class="cv-kpi">
<div class="cv-kpi-label">Confirmate</div>
<div class="cv-kpi-value green">{{ $stats['confirmed_count'] }} / {{ $stats['total_count'] }}</div>
<div class="cv-kpi-sub">{{ $stats['confirmation_rate_pct'] }}% rata confirmare</div>
</div>
<div class="cv-kpi">
<div class="cv-kpi-label">No-show alert</div>
<div class="cv-kpi-value red">{{ $stats['no_show_alert'] }}</div>
<div class="cv-kpi-sub">programări neconfirmate &lt; 24h</div>
</div>
</div>
<div class="cv-toolbar">
<div class="cv-toolbar-group">
<button class="cv-btn cv-btn-icon" wire:click="shiftWeek(-1)"></button>
<div class="cv-date">{{ $this->getWeekLabel() }}</div>
<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">
<button class="{{ $groupBy === 'post' ? 'active' : '' }}" wire:click="setGroupBy('post')">Pod</button>
<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)">
<option value="">Toți mecanicii</option>
@foreach ($this->getMasterOptions() as $id => $name)
<option value="{{ $id }}" {{ $masterFilter == $id ? 'selected' : '' }}>{{ $name }}</option>
@endforeach
</select>
<select class="cv-select" wire:change="setStatusFilter($event.target.value)">
<option value="all">Toate statusurile</option>
<option value="confirmed" @if($statusFilter==='confirmed')selected @endif>Confirmate</option>
<option value="unconfirmed" @if($statusFilter==='unconfirmed')selected @endif>Neconfirmate</option>
<option value="in_work" @if($statusFilter==='in_work')selected @endif>În lucru</option>
</select>
</div>
</div>
<div class="cv-notes">
<strong> Cum funcționează:</strong>
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" 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' : '' }}">
<span class="cv-day-name">{{ $day['name'] }}</span>
<span class="cv-day-num">{{ $day['label'] }}</span>
</div>
@endforeach
@foreach ($rows as $row)
<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'] }} @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>
@foreach ($days as $day)
@php
$cell = $matrix[$row['id']][$day['date']] ?? ['events' => [], 'load_hours' => 0, 'capacity' => $row['capacity_hours']];
$loadClass = '';
$cap = $cell['capacity'] ?: 10;
$loadRatio = $cap > 0 ? $cell['load_hours'] / $cap : 0;
if ($loadRatio >= 0.9) $loadClass = 'full';
elseif ($loadRatio >= 0.5) $loadClass = 'warn';
@endphp
<div class="cv-cell cv-cell-body {{ $day['is_weekend'] ? 'weekend' : '' }} {{ $day['is_closed'] ? 'closed' : '' }}"
@if (! $day['is_closed'])
@dragover.prevent="$el.classList.add('over')"
@dragleave="$el.classList.remove('over')"
@drop.prevent="
$el.classList.remove('over');
if (dragEventId) { $wire.moveEvent(dragEventId, {{ $row['id'] }}, '{{ $day['date'] }}'); dragEventId = null; }
"
@endif>
@if (! $day['is_closed'])
<span class="cv-load {{ $loadClass }}">{{ rtrim(rtrim(number_format($cell['load_hours'], 1), '0'), '.') ?: '0' }}/{{ (int)$cap }}</span>
@foreach ($cell['events'] as $e)
<div class="cv-event"
draggable="true"
style="background:{{ $e['color'] }}22; border-left-color:{{ $e['color'] }}"
wire:click="openEvent({{ $e['id'] }})"
@dragstart="dragEventId = {{ $e['id'] }}; $el.classList.add('dragging')"
@dragend="$el.classList.remove('dragging')">
<div class="cv-event-title" style="color:{{ $e['color'] }}">{{ $e['client_name'] ?: $e['title'] }}</div>
<div class="cv-event-meta">{{ $e['vehicle'] ?: '—' }}@if($e['plate']) · {{ $e['plate'] }}@endif</div>
<div class="cv-event-time">{{ $e['time'] }}@if($e['master_initial']) · {{ $e['master_initial'] }}@endif</div>
</div>
@endforeach
<button class="cv-add" type="button" wire:click="openNewForm({{ $row['id'] }}, '{{ $day['date'] }}')">+</button>
@endif
</div>
@endforeach
@endforeach
</div>
</div>
@endif
<div class="cv-legend">
<h3>Legendă</h3>
<div class="cv-legend-row">
<div>
<div class="cv-legend-col">Mecanici</div>
<div class="cv-legend-items">
@foreach ($this->getMasterOptions() as $id => $name)
@php
$u = \App\Models\Tenant\User::find($id);
$color = $u?->color ?: '#94a3b8';
$spec = $u?->specialization ?: '';
@endphp
<div class="cv-legend-item"><span class="cv-legend-dot" style="background:{{ $color }}"></span> {{ $name }} @if($spec)· {{ $spec }} @endif</div>
@endforeach
</div>
</div>
<div>
<div class="cv-legend-col">Încărcare celulă</div>
<div class="cv-legend-items">
<div class="cv-legend-item"><span class="cv-load" style="position:static;">05h/10</span> liber/ușor</div>
<div class="cv-legend-item"><span class="cv-load warn" style="position:static;">58.5h/10</span> mediu</div>
<div class="cv-legend-item"><span class="cv-load full" style="position:static;">≥9h/10</span> plin</div>
</div>
</div>
<div>
<div class="cv-legend-col">Stare zi</div>
<div class="cv-legend-items">
<div class="cv-legend-item"><span class="cv-legend-dot" style="background:#FAFAFA; border:1px solid var(--cv-border);"></span> Weekend</div>
<div class="cv-legend-item"><span class="cv-legend-dot" style="background:repeating-linear-gradient(45deg,#FAFAFA,#FAFAFA 2px,#F1F5F9 2px,#F1F5F9 4px); border:1px solid var(--cv-border);"></span> Închis (Duminică/sărbătoare)</div>
<div class="cv-legend-item"><span class="cv-legend-dot" style="background:var(--cv-blue-bg); border:1px solid var(--cv-blue);"></span> Astăzi</div>
</div>
</div>
</div>
</div>
<div class="cv-overlay {{ $openEventId ? 'open' : '' }}" wire:click="closeEvent"></div>
<div class="cv-panel {{ $openEventId ? 'open' : '' }}">
@if ($openEvent)
<div class="cv-panel-head">
<div>
<div class="cv-panel-title">{{ $openEvent['title'] ?: $openEvent['client_name'] }}</div>
<div class="cv-panel-sub">{{ $openEvent['date'] }} · {{ $openEvent['time'] }} · {{ ucfirst($openEvent['status']) }}</div>
</div>
<div class="cv-close" wire:click="closeEvent"></div>
</div>
<div class="cv-panel-body">
<div class="cv-two-cols">
@if ($openEvent['client_name'])<div class="cv-pfield"><label>Client</label><div class="cv-pfield-val">{{ $openEvent['client_name'] }}</div></div>@endif
@if ($openEvent['client_phone'])<div class="cv-pfield"><label>Telefon</label><div class="cv-pfield-val" style="color:var(--cv-blue);">{{ $openEvent['client_phone'] }}</div></div>@endif
@if ($openEvent['vehicle'])<div class="cv-pfield"><label>Auto</label><div class="cv-pfield-val">{{ $openEvent['vehicle'] }}</div></div>@endif
@if ($openEvent['plate'])<div class="cv-pfield"><label>Numar</label><div class="cv-pfield-val">{{ $openEvent['plate'] }}</div></div>@endif
@if ($openEvent['master_name'])<div class="cv-pfield"><label>Maistru</label><div class="cv-pfield-val">{{ $openEvent['master_name'] }}</div></div>@endif
@if ($openEvent['post_name'])<div class="cv-pfield"><label>Pod</label><div class="cv-pfield-val">{{ $openEvent['post_name'] }}</div></div>@endif
</div>
@if ($openEvent['notes'])
<div class="cv-pfield"><label>Notițe</label><div class="cv-pfield-val" style="white-space:pre-wrap;">{{ $openEvent['notes'] }}</div></div>
@endif
</div>
<div class="cv-panel-actions">
<button class="cv-btn" wire:click="deleteEvent({{ $openEvent['id'] }})" onclick="return confirm('Ștergi această programare?')">🗑 Șterge</button>
<a class="cv-btn cv-btn-primary" href="{{ route('filament.tenant.resources.appointments.edit', ['record' => $openEvent['id']]) }}"> Editare</a>
</div>
@endif
</div>
<div class="cv-overlay {{ $showNewForm ? 'open' : '' }}" wire:click="$set('showNewForm', false)"></div>
<div class="cv-panel {{ $showNewForm ? 'open' : '' }}">
@if ($showNewForm)
<div class="cv-panel-head">
<div>
<div class="cv-panel-title">Programare nouă</div>
<div class="cv-panel-sub">{{ $newAppt['date'] ?? '' }}</div>
</div>
<div class="cv-close" wire:click="$set('showNewForm', false)"></div>
</div>
<div class="cv-panel-body">
<div class="cv-pfield"><label>Subiect *</label><input wire:model="newAppt.title" type="text" placeholder="Schimb ulei + filtru"></div>
<div class="cv-two-cols">
<div class="cv-pfield"><label>Data *</label><input wire:model="newAppt.date" type="date"></div>
<div class="cv-pfield"><label>Pod</label>
<select wire:model="newAppt.post_id">
<option value=""></option>
@foreach ($this->getPostOptions() as $id => $name)
<option value="{{ $id }}">{{ $name }}</option>
@endforeach
</select>
</div>
<div class="cv-pfield"><label>De la</label><input wire:model="newAppt.time_start" type="time"></div>
<div class="cv-pfield"><label>Până la</label><input wire:model="newAppt.time_end" type="time"></div>
</div>
<div class="cv-pfield"><label>Client</label>
<select wire:model.live="newAppt.client_id">
<option value=""></option>
@foreach ($this->getClientOptions() as $id => $name)
<option value="{{ $id }}">{{ $name }}</option>
@endforeach
</select>
</div>
<div class="cv-pfield"><label>Maistru</label>
<select wire:model="newAppt.master_id">
<option value=""></option>
@foreach ($this->getMasterOptions() as $id => $name)
<option value="{{ $id }}">{{ $name }}</option>
@endforeach
</select>
</div>
<div class="cv-pfield"><label>Notițe</label><textarea wire:model="newAppt.notes" rows="3"></textarea></div>
</div>
<div class="cv-panel-actions">
<button class="cv-btn" wire:click="$set('showNewForm', false)">Anulează</button>
<button class="cv-btn cv-btn-primary" wire:click="createAppt">Salvează</button>
</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>