3c92ea5475
Registry grows from 14 → 25 hints. New entries: - wo.dashboard.topbar_actions / meta / tabs / history / finance / timeline / bottom - wo.create.overview - crm.clients.overview / vehicles.overview - finante.dashboard.stats / expenses.overview "?" icons injected into: - WO dashboard: top-bar actions, meta header, tabs bar, history card, finance card, timeline card, bottom action bar (in addition to the existing dashboard title, PDF preview, chat) - CalendarBoard: title - PipelineBoard: title - Excel import wizard: step 1 - Main tenant dashboard (via PanelsRenderHook::PAGE_START scoped to Dashboard::class): small banner with a "?" and a shortcut to the FAQ page for users who don't see the icons Users now see hint icons on the primary daily-use screens; more can be added by dropping <x-hint key="..." /> anywhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
538 lines
35 KiB
PHP
538 lines
35 KiB
PHP
<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') }}<x-hint key="crm.calendar.overview" /></h1>
|
||
</div>
|
||
<div class="cv-btn-group">
|
||
@php
|
||
$_days = $this->getDays();
|
||
$_from = $_days[0]['date'] ?? today()->toDateString();
|
||
$_to = end($_days)['date'] ?? today()->toDateString();
|
||
$_pdfUrl = url('/app/appointments/pdf?from=' . $_from . '&to=' . $_to);
|
||
$_pdfDl = $_pdfUrl . '&download=1';
|
||
@endphp
|
||
<div x-data="{ pdfOpen: false }" style="display:inline-block;">
|
||
<button type="button" class="cv-btn" @click="pdfOpen = true">{{ __('🖨 PDF programări') }}</button>
|
||
<div x-show="pdfOpen" x-cloak
|
||
style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px;"
|
||
@click.self="pdfOpen = false"
|
||
@keydown.escape.window="pdfOpen = false">
|
||
<div style="background:#fff;border-radius:8px;width:100%;max-width:1000px;height:90vh;display:flex;flex-direction:column;overflow:hidden;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #e5e7eb;">
|
||
<b style="font-size:14px;">{{ __('Programări') }} — {{ $_from }} → {{ $_to }}</b>
|
||
<div style="display:flex;gap:12px;align-items:center;">
|
||
<a href="{{ $_pdfDl }}" style="font-size:13px;color:#2563eb;text-decoration:none;">⬇️ {{ __('Descarcă') }}</a>
|
||
<a href="{{ $_pdfUrl }}" target="_blank" style="font-size:13px;color:#2563eb;text-decoration:none;">↗️ {{ __('Deschide în tab nou') }}</a>
|
||
<button type="button" @click="pdfOpen = false" style="background:none;border:none;font-size:20px;cursor:pointer;color:#6b7280;">✕</button>
|
||
</div>
|
||
</div>
|
||
<iframe x-bind:src="pdfOpen ? '{{ $_pdfUrl }}#toolbar=1&navpanes=0' : ''"
|
||
style="flex:1;width:100%;border:none;" title="PDF"></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<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 < 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)· {{ implode(' / ', array_map(fn($p) => __(trim($p)), explode('/', $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;">{{ __('0–5h/10') }}</span> {{ __('liber/ușor') }}</div>
|
||
<div class="cv-legend-item"><span class="cv-load warn" style="position:static;">{{ __('5–8.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>
|