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:
@@ -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')
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
Reference in New Issue
Block a user