feat(work-orders): new Mitchell1-style dashboard view (Phase 1 — layout + tabs)

Custom Filament Page at /app/work-orders/{id}/dashboard renders a
Mitchell1-inspired 3-column layout:
  - Top bar: WO#, status badge, actions (Edit / Tracking link)
  - Meta header row: creation / opened / ETA / responsible / urgency /
    paid amount (6 cells)
  - Left sidebar (300px): Client card (avatar, phone, email, status
    tag, 3-stat grid: visits/total/debt) + Vehicle card (photo, plate,
    VIN, mileage, engine, gearbox) + Repair history (latest 5 for this
    vehicle, links to dashboards)
  - Middle: tab bar (Lucrări/Piese/Diagnostic/Foto/Documente/Note)
    with Alpine-driven switching. Works & Parts show the tables read-
    only; add/edit still goes through existing EditWorkOrder Filament
    resource. Photos tab shows gallery from spatie/media-library.
  - Right (300px): Finance summary card (works cost, parts cost,
    discount, total, paid, balance) + placeholder for Timeline+Chat
    (Phase 2)
  - Responsive: right column collapses <1280px, left <900px.

'Vizualizare dashboard' button added on top of the existing
EditWorkOrder page so users can switch between edit form and info-
dense dashboard.

Fixed getSlug() signature (must match parent with ?Panel $panel).

+14 translations. All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 18:48:52 +00:00
parent 6de9c615f7
commit fa8704f8b6
5 changed files with 818 additions and 2 deletions
@@ -0,0 +1,92 @@
<?php
namespace App\Filament\Tenant\Pages;
use App\Models\Tenant\WorkOrder;
use Filament\Pages\Page;
/**
* Mitchell1-style 3-column dashboard view for a single WorkOrder.
* MVP Phase 1: layout + tabs (Works/Parts/Diagnostic/Photos/Docs/Notes).
* Editing still goes through /app/work-orders/{id}/edit for now.
*/
class WorkOrderDashboard extends Page
{
protected static string|\BackedEnum|null $navigationIcon = null;
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.tenant.pages.work-order-dashboard';
public WorkOrder $record;
public string $activeTab = 'works';
public static function getSlug(?\Filament\Panel $panel = null): string
{
return 'work-orders/{record}/dashboard';
}
public static function getRoutePath(?\Filament\Panel $panel = null): string
{
return 'work-orders/{record}/dashboard';
}
public function mount(int|string $record): void
{
$this->record = WorkOrder::with([
'client', 'vehicle', 'master',
'works.labor', 'works.master',
'parts.part', 'parts.batch',
'payments.user',
'subcontractJobs.subcontractor',
])->findOrFail($record);
}
public function setTab(string $tab): void
{
$this->activeTab = $tab;
}
public function getTitle(): string|\Illuminate\Contracts\Support\Htmlable
{
return __('Fișă') . ' ' . ($this->record->number ?? '');
}
public function getHeading(): string|\Illuminate\Contracts\Support\Htmlable
{
return '';
}
/** Latest 5 WOs for the vehicle (repair history sidebar). */
public function getRepairHistory(): \Illuminate\Support\Collection
{
if (! $this->record->vehicle_id) return collect();
return WorkOrder::where('vehicle_id', $this->record->vehicle_id)
->where('id', '!=', $this->record->id)
->orderByDesc('opened_at')
->limit(5)
->get(['id', 'number', 'opened_at', 'total', 'status']);
}
/** Aggregated finance data — used by right panel in future phases. */
public function getFinance(): array
{
$r = $this->record;
$worksSum = (float) $r->works->sum('total');
$partsSum = (float) $r->parts->sum('total');
$subtotal = $worksSum + $partsSum;
$discount = (float) $r->discount;
$total = max(0, $subtotal - $discount);
$paid = $r->paidAmount();
return [
'works_sum' => $worksSum,
'parts_sum' => $partsSum,
'subtotal' => $subtotal,
'discount' => $discount,
'total' => $total,
'paid' => $paid,
'balance' => max(0, $total - $paid),
];
}
}
@@ -32,6 +32,11 @@ class EditWorkOrder extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\Action::make('dashboard')
->label(__('Vizualizare dashboard'))
->icon('heroicon-m-squares-2x2')
->color('info')
->url(fn () => route('filament.tenant.pages.work-order-dashboard', ['record' => $this->record->id])),
Actions\Action::make('apply_template') Actions\Action::make('apply_template')
->label(__('Aplică șablon')) ->label(__('Aplică șablon'))
->icon('heroicon-m-clipboard-document-list') ->icon('heroicon-m-clipboard-document-list')
+12
View File
@@ -522,6 +522,8 @@
"Cost & marjă": "Cost & margin", "Cost & marjă": "Cost & margin",
"Cost (de la terț)": "Cost (subcontractor)", "Cost (de la terț)": "Cost (subcontractor)",
"Cost (terț)": "Cost (subcontractor)", "Cost (terț)": "Cost (subcontractor)",
"Cost manopere": "Labor cost",
"Cost piese": "Parts cost",
"Cost piese (achiziție)": "Parts cost (purchase)", "Cost piese (achiziție)": "Parts cost (purchase)",
"Cost/lead": "Cost/lead", "Cost/lead": "Cost/lead",
"Costuri": "Costs", "Costuri": "Costs",
@@ -891,6 +893,7 @@
"Fără articol (manual)": "No article (manual)", "Fără articol (manual)": "No article (manual)",
"Fără client": "No client", "Fără client": "No client",
"Fără maistru": "No master", "Fără maistru": "No master",
"Fără mașină": "No vehicle",
"Fără răspuns": "No answer", "Fără răspuns": "No answer",
"Fără stoc": "Out of stock", "Fără stoc": "Out of stock",
"GNC": "CNG", "GNC": "CNG",
@@ -996,6 +999,7 @@
"Is vip": "VIP", "Is vip": "VIP",
"Istoric depozitare": "Storage history", "Istoric depozitare": "Storage history",
"Istoric prețuri furnizori": "Supplier price history", "Istoric prețuri furnizori": "Supplier price history",
"Istoric reparații": "Repair history",
"Iulie": "July", "Iulie": "July",
"Iunie": "June", "Iunie": "June",
"JSON": "JSON", "JSON": "JSON",
@@ -1246,11 +1250,13 @@
"Nicio factură generată": "No invoice generated", "Nicio factură generată": "No invoice generated",
"Nicio fișă de lucru": "No work orders", "Nicio fișă de lucru": "No work orders",
"Nicio fișă în perioada selectată.": "No orders in selected period.", "Nicio fișă în perioada selectată.": "No orders in selected period.",
"Nicio fotografie": "No photos",
"Nicio lucrare de caroserie": "No bodyshop jobs", "Nicio lucrare de caroserie": "No bodyshop jobs",
"Nicio lucrare la terți": "No subcontracted work", "Nicio lucrare la terți": "No subcontracted work",
"Nicio manoperă efectuată.": "No work performed.", "Nicio manoperă efectuată.": "No work performed.",
"Nicio mașină încă": "No vehicles yet", "Nicio mașină încă": "No vehicles yet",
"Nicio metodă de plată configurată. Contactează operatorul.": "No payment method configured. Contact operator.", "Nicio metodă de plată configurată. Contactează operatorul.": "No payment method configured. Contact operator.",
"Nicio notă internă.": "No internal notes.",
"Nicio piesă implicită": "No default parts", "Nicio piesă implicită": "No default parts",
"Nicio piesă montată.": "No parts installed.", "Nicio piesă montată.": "No parts installed.",
"Nicio plată": "No payments", "Nicio plată": "No payments",
@@ -1558,6 +1564,7 @@
"Prețuri": "Prices", "Prețuri": "Prices",
"Prețurile se înregistrează automat la fiecare recepție de PO.": "Prices are auto-recorded on every PO receipt.", "Prețurile se înregistrează automat la fiecare recepție de PO.": "Prices are auto-recorded on every PO receipt.",
"Price": "Price", "Price": "Price",
"Prima vizită": "First visit",
"Primește cereri din canalul Telegram + trimite confirmări automate.": "Receive requests from the Telegram channel + send auto-confirmations.", "Primește cereri din canalul Telegram + trimite confirmări automate.": "Receive requests from the Telegram channel + send auto-confirmations.",
"Primit": "Incoming", "Primit": "Incoming",
"Print": "Print", "Print": "Print",
@@ -1980,6 +1987,7 @@
"Testează bot Telegram": "Test Telegram bot", "Testează bot Telegram": "Test Telegram bot",
"Theme color": "Theme color", "Theme color": "Theme color",
"Time": "Time", "Time": "Time",
"Timeline & Chat — Phase 2": "Timeline & Chat — Phase 2",
"Timp deschidere, ms": "Opening time, ms", "Timp deschidere, ms": "Opening time, ms",
"Timp lucrat": "Time worked", "Timp lucrat": "Time worked",
"Timp mediu": "Avg time", "Timp mediu": "Avg time",
@@ -2030,8 +2038,10 @@
"Total general": "Grand total", "Total general": "Grand total",
"Total ieșiri": "Total outflow", "Total ieșiri": "Total outflow",
"Total intrări": "Total inflow", "Total intrări": "Total inflow",
"Total manopere:": "Labor total:",
"Total net": "Net total", "Total net": "Net total",
"Total paid": "Total paid", "Total paid": "Total paid",
"Total piese:": "Parts total:",
"Total plătit": "Total paid", "Total plătit": "Total paid",
"Total rezultate": "Total results", "Total rezultate": "Total results",
"Total venituri": "Total revenue", "Total venituri": "Total revenue",
@@ -2178,7 +2188,9 @@
"Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe", "Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe",
"Vizibil": "Visible", "Vizibil": "Visible",
"Vizitator": "Viewer", "Vizitator": "Viewer",
"Vizite": "Visits",
"Vizual": "Visual", "Vizual": "Visual",
"Vizualizare dashboard": "Dashboard view",
"Vizualizează": "View", "Vizualizează": "View",
"Vizualizări": "Views", "Vizualizări": "Views",
"Vopsea sărită": "Paint chip", "Vopsea sărită": "Paint chip",
+14 -2
View File
@@ -522,6 +522,8 @@
"Cost & marjă": "Стоимость и маржа", "Cost & marjă": "Стоимость и маржа",
"Cost (de la terț)": "Стоимость (от субподрядчика)", "Cost (de la terț)": "Стоимость (от субподрядчика)",
"Cost (terț)": "Стоимость (субподр.)", "Cost (terț)": "Стоимость (субподр.)",
"Cost manopere": "Стоимость работ",
"Cost piese": "Стоимость запчастей",
"Cost piese (achiziție)": "Стоимость запчастей (закупка)", "Cost piese (achiziție)": "Стоимость запчастей (закупка)",
"Cost/lead": "Стоимость/заявка", "Cost/lead": "Стоимость/заявка",
"Costuri": "Затраты", "Costuri": "Затраты",
@@ -563,7 +565,7 @@
"Curățare": "Чистка", "Curățare": "Чистка",
"Curăță": "Очистить", "Curăță": "Очистить",
"Custom": "Свой", "Custom": "Свой",
"Cutie": "Коробка", "Cutie": "КПП",
"Cutie de viteze": "Коробка передач", "Cutie de viteze": "Коробка передач",
"Cutie viteze": "КПП", "Cutie viteze": "КПП",
"Câmp obligatoriu": "Обязательное поле", "Câmp obligatoriu": "Обязательное поле",
@@ -891,6 +893,7 @@
"Fără articol (manual)": "Без артикула (вручную)", "Fără articol (manual)": "Без артикула (вручную)",
"Fără client": "Без клиента", "Fără client": "Без клиента",
"Fără maistru": "Без мастера", "Fără maistru": "Без мастера",
"Fără mașină": "Без авто",
"Fără răspuns": "Без ответа", "Fără răspuns": "Без ответа",
"Fără stoc": "Нет в наличии", "Fără stoc": "Нет в наличии",
"GNC": "CNG", "GNC": "CNG",
@@ -996,6 +999,7 @@
"Is vip": "VIP", "Is vip": "VIP",
"Istoric depozitare": "История хранения", "Istoric depozitare": "История хранения",
"Istoric prețuri furnizori": "История цен поставщиков", "Istoric prețuri furnizori": "История цен поставщиков",
"Istoric reparații": "История ремонтов",
"Iulie": "Июль", "Iulie": "Июль",
"Iunie": "Июнь", "Iunie": "Июнь",
"JSON": "JSON", "JSON": "JSON",
@@ -1246,11 +1250,13 @@
"Nicio factură generată": "Счета не созданы", "Nicio factură generată": "Счета не созданы",
"Nicio fișă de lucru": "Нет заказ-нарядов", "Nicio fișă de lucru": "Нет заказ-нарядов",
"Nicio fișă în perioada selectată.": "Нет нарядов за выбранный период.", "Nicio fișă în perioada selectată.": "Нет нарядов за выбранный период.",
"Nicio fotografie": "Нет фотографий",
"Nicio lucrare de caroserie": "Нет кузовных работ", "Nicio lucrare de caroserie": "Нет кузовных работ",
"Nicio lucrare la terți": "Нет работ у субподрядчиков", "Nicio lucrare la terți": "Нет работ у субподрядчиков",
"Nicio manoperă efectuată.": "Работы не выполнены.", "Nicio manoperă efectuată.": "Работы не выполнены.",
"Nicio mașină încă": "Пока нет авто", "Nicio mașină încă": "Пока нет авто",
"Nicio metodă de plată configurată. Contactează operatorul.": "Способ оплаты не настроен. Свяжитесь с оператором.", "Nicio metodă de plată configurată. Contactează operatorul.": "Способ оплаты не настроен. Свяжитесь с оператором.",
"Nicio notă internă.": "Нет внутренних заметок.",
"Nicio piesă implicită": "Нет запчастей по умолчанию", "Nicio piesă implicită": "Нет запчастей по умолчанию",
"Nicio piesă montată.": "Запчасти не установлены.", "Nicio piesă montată.": "Запчасти не установлены.",
"Nicio plată": "Нет платежей", "Nicio plată": "Нет платежей",
@@ -1558,6 +1564,7 @@
"Prețuri": "Цены", "Prețuri": "Цены",
"Prețurile se înregistrează automat la fiecare recepție de PO.": "Цены регистрируются автоматически при каждой приёмке PO.", "Prețurile se înregistrează automat la fiecare recepție de PO.": "Цены регистрируются автоматически при каждой приёмке PO.",
"Price": "Цена", "Price": "Цена",
"Prima vizită": "Первый визит",
"Primește cereri din canalul Telegram + trimite confirmări automate.": "Получайте заявки из Telegram-канала + отправляйте автоподтверждения.", "Primește cereri din canalul Telegram + trimite confirmări automate.": "Получайте заявки из Telegram-канала + отправляйте автоподтверждения.",
"Primit": "Входящий", "Primit": "Входящий",
"Print": "Печать", "Print": "Печать",
@@ -1704,7 +1711,7 @@
"Respins": "Отклонено", "Respins": "Отклонено",
"Respins de": "Отклонено", "Respins de": "Отклонено",
"Responsabil": "Ответственный", "Responsabil": "Ответственный",
"Rest de plată": "Осталось к оплате", "Rest de plată": "К оплате",
"Restanță": "Задолженность", "Restanță": "Задолженность",
"Restaurează": "Восстановить", "Restaurează": "Восстановить",
"Restituire": "Restituire", "Restituire": "Restituire",
@@ -1980,6 +1987,7 @@
"Testează bot Telegram": "Тест Telegram-бота", "Testează bot Telegram": "Тест Telegram-бота",
"Theme color": "Theme color", "Theme color": "Theme color",
"Time": "Время", "Time": "Время",
"Timeline & Chat — Phase 2": "Timeline и чат — этап 2",
"Timp deschidere, ms": "Время открытия, мс", "Timp deschidere, ms": "Время открытия, мс",
"Timp lucrat": "Отработано", "Timp lucrat": "Отработано",
"Timp mediu": "Среднее время", "Timp mediu": "Среднее время",
@@ -2030,8 +2038,10 @@
"Total general": "Общий итог", "Total general": "Общий итог",
"Total ieșiri": "Всего расходов", "Total ieșiri": "Всего расходов",
"Total intrări": "Всего поступлений", "Total intrări": "Всего поступлений",
"Total manopere:": "Итого работы:",
"Total net": "Итого чистыми", "Total net": "Итого чистыми",
"Total paid": "Всего оплачено", "Total paid": "Всего оплачено",
"Total piese:": "Итого запчасти:",
"Total plătit": "Всего оплачено", "Total plătit": "Всего оплачено",
"Total rezultate": "Всего результатов", "Total rezultate": "Всего результатов",
"Total venituri": "Всего доходов", "Total venituri": "Всего доходов",
@@ -2178,7 +2188,9 @@
"Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe", "Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe",
"Vizibil": "Видимо", "Vizibil": "Видимо",
"Vizitator": "Гость", "Vizitator": "Гость",
"Vizite": "Визиты",
"Vizual": "Визуально", "Vizual": "Визуально",
"Vizualizare dashboard": "Дашборд",
"Vizualizează": "Просмотр", "Vizualizează": "Просмотр",
"Vizualizări": "Просмотры", "Vizualizări": "Просмотры",
"Vopsea sărită": "Скол краски", "Vopsea sărită": "Скол краски",
@@ -0,0 +1,695 @@
<x-filament-panels::page>
@php
/** @var \App\Models\Tenant\WorkOrder $wo */
$wo = $this->record;
$finance = $this->getFinance();
$history = $this->getRepairHistory();
$tabs = [
'works' => ['label' => __('Lucrări'), 'count' => $wo->works->count()],
'parts' => ['label' => __('Piese'), 'count' => $wo->parts->count()],
'diag' => ['label' => __('Diagnostic'), 'count' => null],
'photos' => ['label' => __('Foto'), 'count' => $wo->getMedia('photos')->count() ?? 0],
'docs' => ['label' => __('Documente'), 'count' => null],
'notes' => ['label' => __('Note'), 'count' => null],
];
$statusClass = match ($wo->status) {
'in_work' => 'wd-status-active',
'awaiting_parts' => 'wd-status-warn',
'ready' => 'wd-status-ready',
'done' => 'wd-status-done',
'cancelled' => 'wd-status-cancel',
default => 'wd-status-new',
};
@endphp
<style>
/* ══ WORK-ORDER DASHBOARD (Mitchell1-style) ══ */
:root {
--wd-bg: #F7F7F5;
--wd-surface: #FFFFFF;
--wd-border: #E5E7EB;
--wd-border-md: #D1D5DB;
--wd-text: #111827;
--wd-text-2: #4B5563;
--wd-text-3: #9CA3AF;
--wd-blue: #2563EB;
--wd-blue-bg: #EFF6FF;
--wd-green: #16A34A;
--wd-green-bg: #DCFCE7;
--wd-amber: #D97706;
--wd-amber-bg: #FEF3C7;
--wd-red: #DC2626;
--wd-red-bg: #FEE2E2;
--wd-slate: #64748B;
}
.dark {
--wd-bg: #0F172A;
--wd-surface: #1E293B;
--wd-border: #334155;
--wd-border-md: #475569;
--wd-text: #F1F5F9;
--wd-text-2: #CBD5E1;
--wd-text-3: #94A3B8;
}
/* Break out of Filament content wrapper */
.fi-main-ctn:has(.wd-shell) { padding: 0 !important; }
.fi-main:has(.wd-shell) { padding: 0 !important; }
.fi-page:has(.wd-shell) > div { padding: 0 !important; gap: 0 !important; }
.fi-page:has(.wd-shell) .fi-header { display: none !important; }
.wd-shell {
background: var(--wd-bg);
color: var(--wd-text);
min-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
font-size: 13px;
}
/* ── TOP BAR ── */
.wd-topbar {
background: var(--wd-surface);
border-bottom: 1px solid var(--wd-border);
padding: 10px 20px;
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.wd-wo-title { font-size: 15px; font-weight: 700; letter-spacing: -.2px; }
.wd-status {
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .3px;
cursor: pointer;
display: inline-flex; align-items: center; gap: 4px;
}
.wd-status-active { background: #DBEAFE; color: #1E40AF; }
.wd-status-warn { background: var(--wd-amber-bg); color: var(--wd-amber); }
.wd-status-ready { background: var(--wd-green-bg); color: #166534; }
.wd-status-done { background: #F3F4F6; color: #6B7280; }
.wd-status-cancel { background: var(--wd-red-bg); color: var(--wd-red); }
.wd-status-new { background: #EDE9FE; color: #6D28D9; }
.wd-btn {
padding: 7px 12px;
border: 1px solid var(--wd-border-md);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
background: var(--wd-surface);
color: var(--wd-text);
cursor: pointer;
display: inline-flex; align-items: center; gap: 5px;
text-decoration: none;
}
.wd-btn:hover { background: var(--wd-bg); }
.wd-btn-primary { background: var(--wd-green); color: #fff; border-color: var(--wd-green); }
.wd-btn-primary:hover { background: #15803d; }
.wd-btn-secondary { background: var(--wd-text); color: #fff; border-color: var(--wd-text); }
.wd-btn-secondary:hover { background: #000; }
.wd-topbar-actions { margin-left: auto; display: flex; gap: 6px; }
/* ── META HEADER ROW ── */
.wd-meta {
background: var(--wd-surface);
border-bottom: 1px solid var(--wd-border);
padding: 10px 20px;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
flex-shrink: 0;
}
.wd-meta-item { display: flex; flex-direction: column; gap: 3px; }
.wd-meta-lbl { font-size: 10px; color: var(--wd-text-3); text-transform: uppercase; letter-spacing: .4px; font-weight: 600; }
.wd-meta-val { font-size: 13px; color: var(--wd-text); }
.wd-meta-val.person { display: flex; align-items: center; gap: 6px; }
.wd-avatar {
width: 22px; height: 22px; border-radius: 50%;
background: var(--wd-blue-bg); color: var(--wd-blue);
display: inline-flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700;
}
.wd-priority-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
margin-right: 5px;
}
.wd-priority-normal { background: var(--wd-amber); }
.wd-priority-urgent, .wd-priority-express { background: var(--wd-red); }
/* ── 3-COLUMN GRID ── */
.wd-body {
flex: 1;
display: grid;
grid-template-columns: 300px 1fr 300px;
gap: 12px;
padding: 12px;
min-height: 0;
overflow: hidden;
}
@media (max-width: 1280px) {
.wd-body { grid-template-columns: 260px 1fr; }
.wd-col-right { display: none; }
}
@media (max-width: 900px) {
.wd-body { grid-template-columns: 1fr; }
.wd-col-left { display: none; }
}
.wd-col-left, .wd-col-mid, .wd-col-right {
display: flex; flex-direction: column; gap: 12px;
overflow-y: auto;
min-height: 0;
}
/* ── CARDS (common) ── */
.wd-card {
background: var(--wd-surface);
border: 1px solid var(--wd-border);
border-radius: 10px;
padding: 14px;
}
.wd-card-hd {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.wd-card-hd h3 { font-size: 13px; font-weight: 600; margin: 0; }
.wd-card-hd a { font-size: 11px; color: var(--wd-blue); text-decoration: none; }
.wd-card-hd a:hover { text-decoration: underline; }
/* ── LEFT SIDEBAR ── */
.wd-person-row {
display: flex; align-items: center; gap: 10px;
margin-bottom: 8px;
}
.wd-person-avatar {
width: 40px; height: 40px; border-radius: 50%;
background: var(--wd-bg); border: 1px solid var(--wd-border);
display: inline-flex; align-items: center; justify-content: center;
font-size: 15px; font-weight: 700; color: var(--wd-text-2);
flex-shrink: 0;
}
.wd-person-name { font-size: 13px; font-weight: 600; line-height: 1.2; }
.wd-person-phone { font-size: 11px; color: var(--wd-blue); display: block; margin-top: 2px; }
.wd-person-email { font-size: 11px; color: var(--wd-text-2); display: block; margin-top: 1px; }
.wd-tag {
display: inline-block;
background: var(--wd-blue-bg); color: var(--wd-blue);
padding: 2px 8px; border-radius: 4px;
font-size: 10px; font-weight: 600;
margin-top: 6px;
}
.wd-stat-grid {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 6px; margin-top: 10px;
}
.wd-stat-cell {
text-align: center;
background: var(--wd-bg);
border-radius: 6px;
padding: 6px 4px;
}
.wd-stat-cell .l { font-size: 9px; color: var(--wd-text-3); text-transform: uppercase; }
.wd-stat-cell .v { font-size: 12px; font-weight: 700; margin-top: 2px; }
.wd-stat-cell .v.money { color: var(--wd-green); }
.wd-stat-cell .v.debt { color: var(--wd-red); }
.wd-vehicle-photo {
width: 100%; aspect-ratio: 16/10; object-fit: cover;
background: var(--wd-bg);
border-radius: 6px;
margin-bottom: 8px;
}
.wd-veh-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
.wd-veh-facts {
display: grid; grid-template-columns: auto 1fr;
gap: 4px 10px; font-size: 11px;
margin-top: 6px;
}
.wd-veh-facts dt { color: var(--wd-text-3); }
.wd-veh-facts dd { color: var(--wd-text); font-weight: 500; margin: 0; }
.wd-history-item {
padding: 6px 0;
border-bottom: 1px solid var(--wd-border);
font-size: 11px;
display: flex; justify-content: space-between; gap: 8px;
}
.wd-history-item:last-child { border-bottom: 0; }
.wd-history-item .h-date { color: var(--wd-text-3); }
.wd-history-item .h-total { font-weight: 600; }
.wd-history-item a { color: var(--wd-text); text-decoration: none; }
.wd-history-item a:hover { color: var(--wd-blue); }
/* ── MIDDLE: TABS ── */
.wd-tabs-bar {
background: var(--wd-surface);
border: 1px solid var(--wd-border);
border-radius: 10px 10px 0 0;
border-bottom: 0;
display: flex;
padding: 0 8px;
overflow-x: auto;
flex-shrink: 0;
}
.wd-tab {
padding: 12px 16px;
border-bottom: 2px solid transparent;
color: var(--wd-text-2);
cursor: pointer;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: .3px;
background: none;
border-left: 0; border-right: 0; border-top: 0;
white-space: nowrap;
display: inline-flex; align-items: center; gap: 5px;
}
.wd-tab.active { color: var(--wd-blue); border-bottom-color: var(--wd-blue); font-weight: 600; }
.wd-tab-count {
background: var(--wd-bg); color: var(--wd-text-2);
padding: 1px 6px; border-radius: 8px;
font-size: 10px; font-weight: 600;
}
.wd-tab-content {
background: var(--wd-surface);
border: 1px solid var(--wd-border);
border-top: 0;
border-radius: 0 0 10px 10px;
padding: 16px;
flex: 1;
overflow: auto;
min-height: 0;
}
/* Tables inside tabs */
.wd-tbl { width: 100%; border-collapse: collapse; font-size: 12px; }
.wd-tbl th, .wd-tbl td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid var(--wd-border);
}
.wd-tbl th {
font-size: 10px; color: var(--wd-text-3);
text-transform: uppercase; letter-spacing: .3px;
font-weight: 600; background: var(--wd-bg);
}
.wd-tbl td.right, .wd-tbl th.right { text-align: right; font-variant-numeric: tabular-nums; }
.wd-tbl tr:last-child td { border-bottom: 0; }
.wd-tbl tr:hover td { background: rgba(37,99,235,.03); }
.wd-mini-badge {
display: inline-block;
padding: 2px 7px; border-radius: 4px;
font-size: 10px; font-weight: 600;
text-transform: uppercase;
}
.wd-mini-todo { background: #F3F4F6; color: #6B7280; }
.wd-mini-progress { background: var(--wd-blue-bg); color: var(--wd-blue); }
.wd-mini-done { background: var(--wd-green-bg); color: #166534; }
.wd-empty { padding: 40px 20px; text-align: center; color: var(--wd-text-3); font-size: 12px; }
.wd-empty-add { margin-top: 8px; }
.wd-footer-row {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 4px; margin-top: 8px;
font-size: 12px;
border-top: 1px solid var(--wd-border);
}
.wd-footer-total { font-weight: 700; font-size: 13px; }
/* ── RIGHT SIDEBAR (placeholder for phase 2) ── */
.wd-placeholder {
padding: 20px;
text-align: center;
color: var(--wd-text-3);
font-size: 11px;
font-style: italic;
}
</style>
<div class="wd-shell" x-data="{ tab: @entangle('activeTab').live }">
{{-- ── TOP BAR ── --}}
<div class="wd-topbar">
<span class="wd-wo-title">{{ __('Fișă') }} {{ $wo->number }}</span>
<span class="wd-status {{ $statusClass }}">
{{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }}
<span style="opacity:.6;"></span>
</span>
<div class="wd-topbar-actions">
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id]) }}">
{{ __('Editează') }}
</a>
@if ($wo->tracking_token)
<a class="wd-btn" target="_blank" href="{{ $wo->trackingUrl() }}">
🔗 {{ __('Link tracking') }}
</a>
@endif
<button type="button" class="wd-btn wd-btn-primary">{{ __('Salvează') }}</button>
</div>
</div>
{{-- ── META HEADER ── --}}
<div class="wd-meta">
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('Data creării') }}</span>
<span class="wd-meta-val">{{ $wo->created_at?->format('d.m.Y H:i') ?? '—' }}</span>
</div>
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('Deschis') }}</span>
<span class="wd-meta-val">{{ $wo->opened_at?->format('d.m.Y H:i') ?? '—' }}</span>
</div>
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('ETA') }}</span>
<span class="wd-meta-val">{{ $wo->eta_at?->format('d.m.Y H:i') ?? '—' }}</span>
</div>
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('Responsabil') }}</span>
<span class="wd-meta-val person">
@if ($wo->master)
<span class="wd-avatar">{{ mb_strtoupper(mb_substr($wo->master->name, 0, 2)) }}</span>
{{ $wo->master->name }}
@else @endif
</span>
</div>
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('Urgență') }}</span>
<span class="wd-meta-val">
<span class="wd-priority-dot wd-priority-{{ $wo->urgency ?? 'normal' }}"></span>
{{ __(\App\Models\Tenant\PricingCoefficient::URGENCY[$wo->urgency ?? 'normal'] ?? 'Normal') }}
</span>
</div>
<div class="wd-meta-item">
<span class="wd-meta-lbl">{{ __('Data plății') }}</span>
<span class="wd-meta-val">{{ $wo->paidAmount() > 0 ? number_format($wo->paidAmount(), 2, '.', ' ') . ' MDL' : '—' }}</span>
</div>
</div>
{{-- ── 3-COLUMN BODY ── --}}
<div class="wd-body">
{{-- ═══ LEFT SIDEBAR ═══ --}}
<div class="wd-col-left">
{{-- CLIENT CARD --}}
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Client') }}</h3>
@if ($wo->client)
<a href="{{ route('filament.tenant.resources.clients.edit', ['record' => $wo->client_id]) }}">{{ __('Editează') }}</a>
@endif
</div>
@if ($wo->client)
<div class="wd-person-row">
<div class="wd-person-avatar">{{ mb_strtoupper(mb_substr($wo->client->name, 0, 1)) }}</div>
<div style="flex:1;min-width:0;">
<div class="wd-person-name">{{ $wo->client->name }}</div>
@if ($wo->client->phone)
<a href="tel:{{ $wo->client->phone }}" class="wd-person-phone">📞 {{ $wo->client->phone }}</a>
@endif
@if ($wo->client->email)
<span class="wd-person-email">{{ $wo->client->email }}</span>
@endif
</div>
</div>
@if (! empty($wo->client->status))
<span class="wd-tag">{{ __(ucfirst($wo->client->status)) }}</span>
@endif
@php
$totalOrders = \App\Models\Tenant\WorkOrder::where('client_id', $wo->client_id)->count();
$totalSpent = (float) \App\Models\Tenant\WorkOrder::where('client_id', $wo->client_id)->sum('total');
$debt = (float) $wo->client->balance;
@endphp
<div class="wd-stat-grid">
<div class="wd-stat-cell">
<div class="l">{{ __('Vizite') }}</div>
<div class="v">{{ $totalOrders }}</div>
</div>
<div class="wd-stat-cell">
<div class="l">{{ __('Total') }}</div>
<div class="v money">{{ number_format($totalSpent, 0, '.', ' ') }}</div>
</div>
<div class="wd-stat-cell">
<div class="l">{{ __('Datorie') }}</div>
<div class="v {{ $debt > 0 ? 'debt' : '' }}">{{ number_format($debt, 0, '.', ' ') }}</div>
</div>
</div>
@else
<div class="wd-empty">{{ __('Fără client') }}</div>
@endif
</div>
{{-- VEHICLE CARD --}}
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Automobil') }}</h3>
@if ($wo->vehicle)
<a href="{{ route('filament.tenant.resources.vehicles.edit', ['record' => $wo->vehicle_id]) }}">{{ __('Editează') }}</a>
@endif
</div>
@if ($wo->vehicle)
@php
$photoUrl = $wo->vehicle->getFirstMediaUrl('photos') ?? null;
@endphp
@if ($photoUrl)
<img class="wd-vehicle-photo" src="{{ $photoUrl }}" alt="{{ $wo->vehicle->make }}">
@else
<div class="wd-vehicle-photo" style="display:flex;align-items:center;justify-content:center;color:var(--wd-text-3);font-size:24px;">🚗</div>
@endif
<div class="wd-veh-title">{{ $wo->vehicle->make }} {{ $wo->vehicle->model }} {{ $wo->vehicle->year }}</div>
<dl class="wd-veh-facts">
@if ($wo->vehicle->plate)
<dt>{{ __('Nr.') }}</dt><dd>{{ $wo->vehicle->plate }}</dd>
@endif
@if ($wo->vehicle->vin)
<dt>VIN</dt><dd style="font-family:monospace;font-size:10px;">{{ $wo->vehicle->vin }}</dd>
@endif
@if ($wo->vehicle->mileage)
<dt>{{ __('Km') }}</dt><dd>{{ number_format($wo->vehicle->mileage, 0, '.', ' ') }}</dd>
@endif
@if ($wo->vehicle->engine)
<dt>{{ __('Motor') }}</dt><dd>{{ $wo->vehicle->engine }}</dd>
@endif
@if ($wo->vehicle->gearbox)
<dt>{{ __('Cutie') }}</dt><dd>{{ $wo->vehicle->gearbox }}</dd>
@endif
</dl>
@else
<div class="wd-empty">{{ __('Fără mașină') }}</div>
@endif
</div>
{{-- REPAIR HISTORY --}}
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Istoric reparații') }}</h3>
</div>
@forelse ($history as $h)
<div class="wd-history-item">
<a href="{{ route('filament.tenant.pages.work-order-dashboard', ['record' => $h->id]) }}">
<span class="h-date">{{ $h->opened_at?->format('d.m.Y') }}</span>
<span>{{ $h->number }}</span>
</a>
<span class="h-total">{{ number_format($h->total, 0, '.', ' ') }} MDL</span>
</div>
@empty
<div class="wd-empty" style="padding:16px 0;">{{ __('Prima vizită') }}</div>
@endforelse
</div>
</div>
{{-- ═══ MIDDLE: TABS ═══ --}}
<div class="wd-col-mid">
<div class="wd-tabs-bar">
@foreach ($tabs as $key => $t)
<button type="button" class="wd-tab" :class="tab === '{{ $key }}' && 'active'" @click="tab = '{{ $key }}'">
{{ $t['label'] }}
@if (! is_null($t['count']))
<span class="wd-tab-count">{{ $t['count'] }}</span>
@endif
</button>
@endforeach
</div>
<div class="wd-tab-content">
{{-- ── WORKS TAB ── --}}
<div x-show="tab === 'works'" x-cloak>
@if ($wo->works->isNotEmpty())
<table class="wd-tbl">
<thead>
<tr>
<th>#</th>
<th>{{ __('Manoperă') }}</th>
<th class="right">{{ __('Ore') }}</th>
<th>{{ __('Mecanic') }}</th>
<th class="right">{{ __('Preț/h') }}</th>
<th class="right">{{ __('Total') }}</th>
<th>{{ __('Status') }}</th>
</tr>
</thead>
<tbody>
@foreach ($wo->works as $i => $w)
<tr>
<td>{{ $i + 1 }}</td>
<td>{{ $w->labor?->label() ?: $w->name }}</td>
<td class="right">{{ rtrim(rtrim(number_format($w->hours, 2), '0'), '.') }}</td>
<td>{{ $w->master?->name ?? '—' }}</td>
<td class="right">{{ number_format($w->price_per_hour, 2, '.', ' ') }}</td>
<td class="right">{{ number_format($w->total, 2, '.', ' ') }}</td>
<td>
<span class="wd-mini-badge wd-mini-{{ $w->status === 'done' ? 'done' : ($w->status === 'in_progress' ? 'progress' : 'todo') }}">
{{ __(\App\Models\Tenant\WorkOrderWork::STATUSES[$w->status] ?? $w->status) }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="wd-footer-row">
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 0]) }}">+ {{ __('Adaugă manoperă') }}</a>
<span class="wd-footer-total">{{ __('Total manopere:') }} {{ number_format($finance['works_sum'], 2, '.', ' ') }} MDL</span>
</div>
@else
<div class="wd-empty">
{{ __('Nicio manoperă efectuată.') }}
<div class="wd-empty-add"><a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 0]) }}">+ {{ __('Adaugă manoperă') }}</a></div>
</div>
@endif
</div>
{{-- ── PARTS TAB ── --}}
<div x-show="tab === 'parts'" x-cloak>
@if ($wo->parts->isNotEmpty())
<table class="wd-tbl">
<thead>
<tr>
<th>{{ __('Cod') }}</th>
<th>{{ __('Denumire') }}</th>
<th>{{ __('Brand') }}</th>
<th class="right">{{ __('Cant.') }}</th>
<th class="right">{{ __('Preț') }}</th>
<th class="right">{{ __('Total') }}</th>
<th>{{ __('Status') }}</th>
</tr>
</thead>
<tbody>
@foreach ($wo->parts as $p)
<tr>
<td style="font-family:monospace;font-size:11px;">{{ $p->article ?? '—' }}</td>
<td>{{ $p->name }}</td>
<td>{{ $p->brand ?? '—' }}</td>
<td class="right">{{ rtrim(rtrim(number_format($p->qty, 2), '0'), '.') }} {{ $p->unitLabel() ?: '' }}</td>
<td class="right">{{ number_format($p->sell_price, 2, '.', ' ') }}</td>
<td class="right">{{ number_format($p->total, 2, '.', ' ') }}</td>
<td>
<span class="wd-mini-badge wd-mini-{{ $p->status === 'installed' ? 'done' : ($p->status === 'delivered' ? 'progress' : 'todo') }}">
{{ __(\App\Models\Tenant\WorkOrderPart::STATUSES[$p->status] ?? $p->status) }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="wd-footer-row">
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 1]) }}">+ {{ __('Adaugă piesă') }}</a>
<span class="wd-footer-total">{{ __('Total piese:') }} {{ number_format($finance['parts_sum'], 2, '.', ' ') }} MDL</span>
</div>
@else
<div class="wd-empty">
{{ __('Nicio piesă montată.') }}
<div class="wd-empty-add"><a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 1]) }}">+ {{ __('Adaugă piesă') }}</a></div>
</div>
@endif
</div>
{{-- ── DIAGNOSTIC TAB ── --}}
<div x-show="tab === 'diag'" x-cloak>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div>
<h4 style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin:0 0 8px;">{{ __('Plângere client') }}</h4>
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:60px;">{{ $wo->complaint ?: '—' }}</div>
</div>
<div>
<h4 style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin:0 0 8px;">{{ __('Diagnostic') }}</h4>
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:60px;">{{ $wo->diagnosis ?: '—' }}</div>
</div>
</div>
</div>
{{-- ── PHOTOS TAB ── --}}
<div x-show="tab === 'photos'" x-cloak>
@php $photos = $wo->getMedia('photos'); @endphp
@if ($photos->isNotEmpty())
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;">
@foreach ($photos as $ph)
<a href="{{ $ph->getFullUrl() }}" target="_blank">
<img src="{{ $ph->getFullUrl() }}" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;border:1px solid var(--wd-border);">
</a>
@endforeach
</div>
@else
<div class="wd-empty">{{ __('Nicio fotografie') }}</div>
@endif
</div>
{{-- ── DOCUMENTS TAB ── --}}
<div x-show="tab === 'docs'" x-cloak>
<div class="wd-empty">{{ __('Nicio factură emisă încă') }}</div>
</div>
{{-- ── NOTES TAB ── --}}
<div x-show="tab === 'notes'" x-cloak>
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:80px;">{{ $wo->notes ?: __('Nicio notă internă.') }}</div>
</div>
</div>
</div>
{{-- ═══ RIGHT COLUMN (placeholder Phase 2) ═══ --}}
<div class="wd-col-right">
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Finanțe') }}</h3>
</div>
<div style="display:flex;flex-direction:column;gap:6px;font-size:12px;">
<div style="display:flex;justify-content:space-between;">
<span style="color:var(--wd-text-2);">{{ __('Cost manopere') }}</span>
<b>{{ number_format($finance['works_sum'], 2, '.', ' ') }}</b>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="color:var(--wd-text-2);">{{ __('Cost piese') }}</span>
<b>{{ number_format($finance['parts_sum'], 2, '.', ' ') }}</b>
</div>
@if ($finance['discount'] > 0)
<div style="display:flex;justify-content:space-between;">
<span style="color:var(--wd-text-2);">{{ __('Discount') }}</span>
<b style="color:var(--wd-red);">{{ number_format($finance['discount'], 2, '.', ' ') }}</b>
</div>
@endif
<div style="border-top:1px solid var(--wd-border);padding-top:8px;display:flex;justify-content:space-between;font-size:15px;">
<span>{{ __('Total') }}</span>
<b>{{ number_format($finance['total'], 2, '.', ' ') }} MDL</b>
</div>
<div style="display:flex;justify-content:space-between;color:var(--wd-green);">
<span>{{ __('Achitat') }}</span>
<b>{{ number_format($finance['paid'], 2, '.', ' ') }}</b>
</div>
@if ($finance['balance'] > 0)
<div style="display:flex;justify-content:space-between;color:var(--wd-red);font-weight:700;">
<span>{{ __('Rest de plată') }}</span>
<b>{{ number_format($finance['balance'], 2, '.', ' ') }} MDL</b>
</div>
@endif
</div>
</div>
<div class="wd-placeholder">
{{ __('Timeline & Chat — Phase 2') }}
</div>
</div>
</div>
</div>
</x-filament-panels::page>