diff --git a/app/Filament/Tenant/Pages/WorkOrderDashboard.php b/app/Filament/Tenant/Pages/WorkOrderDashboard.php new file mode 100644 index 0000000..907d1dc --- /dev/null +++ b/app/Filament/Tenant/Pages/WorkOrderDashboard.php @@ -0,0 +1,92 @@ +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), + ]; + } +} diff --git a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php index cdf55ca..c61c080 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php @@ -32,6 +32,11 @@ class EditWorkOrder extends EditRecord protected function getHeaderActions(): array { 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') ->label(__('Aplică șablon')) ->icon('heroicon-m-clipboard-document-list') diff --git a/lang/en.json b/lang/en.json index cf0255d..48cd925 100644 --- a/lang/en.json +++ b/lang/en.json @@ -522,6 +522,8 @@ "Cost & marjă": "Cost & margin", "Cost (de la terț)": "Cost (subcontractor)", "Cost (terț)": "Cost (subcontractor)", + "Cost manopere": "Labor cost", + "Cost piese": "Parts cost", "Cost piese (achiziție)": "Parts cost (purchase)", "Cost/lead": "Cost/lead", "Costuri": "Costs", @@ -891,6 +893,7 @@ "Fără articol (manual)": "No article (manual)", "Fără client": "No client", "Fără maistru": "No master", + "Fără mașină": "No vehicle", "Fără răspuns": "No answer", "Fără stoc": "Out of stock", "GNC": "CNG", @@ -996,6 +999,7 @@ "Is vip": "VIP", "Istoric depozitare": "Storage history", "Istoric prețuri furnizori": "Supplier price history", + "Istoric reparații": "Repair history", "Iulie": "July", "Iunie": "June", "JSON": "JSON", @@ -1246,11 +1250,13 @@ "Nicio factură generată": "No invoice generated", "Nicio fișă de lucru": "No work orders", "Nicio fișă în perioada selectată.": "No orders in selected period.", + "Nicio fotografie": "No photos", "Nicio lucrare de caroserie": "No bodyshop jobs", "Nicio lucrare la terți": "No subcontracted work", "Nicio manoperă efectuată.": "No work performed.", "Nicio mașină încă": "No vehicles yet", "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ă montată.": "No parts installed.", "Nicio plată": "No payments", @@ -1558,6 +1564,7 @@ "Prețuri": "Prices", "Prețurile se înregistrează automat la fiecare recepție de PO.": "Prices are auto-recorded on every PO receipt.", "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.", "Primit": "Incoming", "Print": "Print", @@ -1980,6 +1987,7 @@ "Testează bot Telegram": "Test Telegram bot", "Theme color": "Theme color", "Time": "Time", + "Timeline & Chat — Phase 2": "Timeline & Chat — Phase 2", "Timp deschidere, ms": "Opening time, ms", "Timp lucrat": "Time worked", "Timp mediu": "Avg time", @@ -2030,8 +2038,10 @@ "Total general": "Grand total", "Total ieșiri": "Total outflow", "Total intrări": "Total inflow", + "Total manopere:": "Labor total:", "Total net": "Net total", "Total paid": "Total paid", + "Total piese:": "Parts total:", "Total plătit": "Total paid", "Total rezultate": "Total results", "Total venituri": "Total revenue", @@ -2178,7 +2188,9 @@ "Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe", "Vizibil": "Visible", "Vizitator": "Viewer", + "Vizite": "Visits", "Vizual": "Visual", + "Vizualizare dashboard": "Dashboard view", "Vizualizează": "View", "Vizualizări": "Views", "Vopsea sărită": "Paint chip", diff --git a/lang/ru.json b/lang/ru.json index 2db5dc6..5add77a 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -522,6 +522,8 @@ "Cost & marjă": "Стоимость и маржа", "Cost (de la terț)": "Стоимость (от субподрядчика)", "Cost (terț)": "Стоимость (субподр.)", + "Cost manopere": "Стоимость работ", + "Cost piese": "Стоимость запчастей", "Cost piese (achiziție)": "Стоимость запчастей (закупка)", "Cost/lead": "Стоимость/заявка", "Costuri": "Затраты", @@ -563,7 +565,7 @@ "Curățare": "Чистка", "Curăță": "Очистить", "Custom": "Свой", - "Cutie": "Коробка", + "Cutie": "КПП", "Cutie de viteze": "Коробка передач", "Cutie viteze": "КПП", "Câmp obligatoriu": "Обязательное поле", @@ -891,6 +893,7 @@ "Fără articol (manual)": "Без артикула (вручную)", "Fără client": "Без клиента", "Fără maistru": "Без мастера", + "Fără mașină": "Без авто", "Fără răspuns": "Без ответа", "Fără stoc": "Нет в наличии", "GNC": "CNG", @@ -996,6 +999,7 @@ "Is vip": "VIP", "Istoric depozitare": "История хранения", "Istoric prețuri furnizori": "История цен поставщиков", + "Istoric reparații": "История ремонтов", "Iulie": "Июль", "Iunie": "Июнь", "JSON": "JSON", @@ -1246,11 +1250,13 @@ "Nicio factură generată": "Счета не созданы", "Nicio fișă de lucru": "Нет заказ-нарядов", "Nicio fișă în perioada selectată.": "Нет нарядов за выбранный период.", + "Nicio fotografie": "Нет фотографий", "Nicio lucrare de caroserie": "Нет кузовных работ", "Nicio lucrare la terți": "Нет работ у субподрядчиков", "Nicio manoperă efectuată.": "Работы не выполнены.", "Nicio mașină încă": "Пока нет авто", "Nicio metodă de plată configurată. Contactează operatorul.": "Способ оплаты не настроен. Свяжитесь с оператором.", + "Nicio notă internă.": "Нет внутренних заметок.", "Nicio piesă implicită": "Нет запчастей по умолчанию", "Nicio piesă montată.": "Запчасти не установлены.", "Nicio plată": "Нет платежей", @@ -1558,6 +1564,7 @@ "Prețuri": "Цены", "Prețurile se înregistrează automat la fiecare recepție de PO.": "Цены регистрируются автоматически при каждой приёмке PO.", "Price": "Цена", + "Prima vizită": "Первый визит", "Primește cereri din canalul Telegram + trimite confirmări automate.": "Получайте заявки из Telegram-канала + отправляйте автоподтверждения.", "Primit": "Входящий", "Print": "Печать", @@ -1704,7 +1711,7 @@ "Respins": "Отклонено", "Respins de": "Отклонено", "Responsabil": "Ответственный", - "Rest de plată": "Осталось к оплате", + "Rest de plată": "К оплате", "Restanță": "Задолженность", "Restaurează": "Восстановить", "Restituire": "Restituire", @@ -1980,6 +1987,7 @@ "Testează bot Telegram": "Тест Telegram-бота", "Theme color": "Theme color", "Time": "Время", + "Timeline & Chat — Phase 2": "Timeline и чат — этап 2", "Timp deschidere, ms": "Время открытия, мс", "Timp lucrat": "Отработано", "Timp mediu": "Среднее время", @@ -2030,8 +2038,10 @@ "Total general": "Общий итог", "Total ieșiri": "Всего расходов", "Total intrări": "Всего поступлений", + "Total manopere:": "Итого работы:", "Total net": "Итого чистыми", "Total paid": "Всего оплачено", + "Total piese:": "Итого запчасти:", "Total plătit": "Всего оплачено", "Total rezultate": "Всего результатов", "Total venituri": "Всего доходов", @@ -2178,7 +2188,9 @@ "Visa, Mastercard prin Stripe": "Visa, Mastercard prin Stripe", "Vizibil": "Видимо", "Vizitator": "Гость", + "Vizite": "Визиты", "Vizual": "Визуально", + "Vizualizare dashboard": "Дашборд", "Vizualizează": "Просмотр", "Vizualizări": "Просмотры", "Vopsea sărită": "Скол краски", diff --git a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php new file mode 100644 index 0000000..de86dc5 --- /dev/null +++ b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php @@ -0,0 +1,695 @@ + +@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 + + + +
+ + {{-- ── TOP BAR ── --}} +
+ {{ __('Fișă') }} {{ $wo->number }} + + {{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }} + + +
+ + ✎ {{ __('Editează') }} + + @if ($wo->tracking_token) + + 🔗 {{ __('Link tracking') }} + + @endif + +
+
+ + {{-- ── META HEADER ── --}} +
+
+ {{ __('Data creării') }} + {{ $wo->created_at?->format('d.m.Y H:i') ?? '—' }} +
+
+ {{ __('Deschis') }} + {{ $wo->opened_at?->format('d.m.Y H:i') ?? '—' }} +
+
+ {{ __('ETA') }} + {{ $wo->eta_at?->format('d.m.Y H:i') ?? '—' }} +
+
+ {{ __('Responsabil') }} + + @if ($wo->master) + {{ mb_strtoupper(mb_substr($wo->master->name, 0, 2)) }} + {{ $wo->master->name }} + @else — @endif + +
+
+ {{ __('Urgență') }} + + + {{ __(\App\Models\Tenant\PricingCoefficient::URGENCY[$wo->urgency ?? 'normal'] ?? 'Normal') }} + +
+
+ {{ __('Data plății') }} + {{ $wo->paidAmount() > 0 ? number_format($wo->paidAmount(), 2, '.', ' ') . ' MDL' : '—' }} +
+
+ + {{-- ── 3-COLUMN BODY ── --}} +
+ {{-- ═══ LEFT SIDEBAR ═══ --}} +
+ {{-- CLIENT CARD --}} +
+
+

{{ __('Client') }}

+ @if ($wo->client) + {{ __('Editează') }} + @endif +
+ @if ($wo->client) +
+
{{ mb_strtoupper(mb_substr($wo->client->name, 0, 1)) }}
+
+
{{ $wo->client->name }}
+ @if ($wo->client->phone) + 📞 {{ $wo->client->phone }} + @endif + @if ($wo->client->email) + {{ $wo->client->email }} + @endif +
+
+ @if (! empty($wo->client->status)) + {{ __(ucfirst($wo->client->status)) }} + @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 +
+
+
{{ __('Vizite') }}
+
{{ $totalOrders }}
+
+
+
{{ __('Total') }}
+
{{ number_format($totalSpent, 0, '.', ' ') }}
+
+
+
{{ __('Datorie') }}
+
{{ number_format($debt, 0, '.', ' ') }}
+
+
+ @else +
{{ __('Fără client') }}
+ @endif +
+ + {{-- VEHICLE CARD --}} +
+
+

{{ __('Automobil') }}

+ @if ($wo->vehicle) + {{ __('Editează') }} + @endif +
+ @if ($wo->vehicle) + @php + $photoUrl = $wo->vehicle->getFirstMediaUrl('photos') ?? null; + @endphp + @if ($photoUrl) + {{ $wo->vehicle->make }} + @else +
🚗
+ @endif +
{{ $wo->vehicle->make }} {{ $wo->vehicle->model }} {{ $wo->vehicle->year }}
+
+ @if ($wo->vehicle->plate) +
{{ __('Nr.') }}
{{ $wo->vehicle->plate }}
+ @endif + @if ($wo->vehicle->vin) +
VIN
{{ $wo->vehicle->vin }}
+ @endif + @if ($wo->vehicle->mileage) +
{{ __('Km') }}
{{ number_format($wo->vehicle->mileage, 0, '.', ' ') }}
+ @endif + @if ($wo->vehicle->engine) +
{{ __('Motor') }}
{{ $wo->vehicle->engine }}
+ @endif + @if ($wo->vehicle->gearbox) +
{{ __('Cutie') }}
{{ $wo->vehicle->gearbox }}
+ @endif +
+ @else +
{{ __('Fără mașină') }}
+ @endif +
+ + {{-- REPAIR HISTORY --}} +
+
+

{{ __('Istoric reparații') }}

+
+ @forelse ($history as $h) +
+ + {{ $h->opened_at?->format('d.m.Y') }} + {{ $h->number }} + + {{ number_format($h->total, 0, '.', ' ') }} MDL +
+ @empty +
{{ __('Prima vizită') }}
+ @endforelse +
+
+ + {{-- ═══ MIDDLE: TABS ═══ --}} +
+
+ @foreach ($tabs as $key => $t) + + @endforeach +
+
+ {{-- ── WORKS TAB ── --}} +
+ @if ($wo->works->isNotEmpty()) + + + + + + + + + + + + + + @foreach ($wo->works as $i => $w) + + + + + + + + + + @endforeach + +
#{{ __('Manoperă') }}{{ __('Ore') }}{{ __('Mecanic') }}{{ __('Preț/h') }}{{ __('Total') }}{{ __('Status') }}
{{ $i + 1 }}{{ $w->labor?->label() ?: $w->name }}{{ rtrim(rtrim(number_format($w->hours, 2), '0'), '.') }}{{ $w->master?->name ?? '—' }}{{ number_format($w->price_per_hour, 2, '.', ' ') }}{{ number_format($w->total, 2, '.', ' ') }} + + {{ __(\App\Models\Tenant\WorkOrderWork::STATUSES[$w->status] ?? $w->status) }} + +
+ + @else +
+ {{ __('Nicio manoperă efectuată.') }} + +
+ @endif +
+ + {{-- ── PARTS TAB ── --}} +
+ @if ($wo->parts->isNotEmpty()) + + + + + + + + + + + + + + @foreach ($wo->parts as $p) + + + + + + + + + + @endforeach + +
{{ __('Cod') }}{{ __('Denumire') }}{{ __('Brand') }}{{ __('Cant.') }}{{ __('Preț') }}{{ __('Total') }}{{ __('Status') }}
{{ $p->article ?? '—' }}{{ $p->name }}{{ $p->brand ?? '—' }}{{ rtrim(rtrim(number_format($p->qty, 2), '0'), '.') }} {{ $p->unitLabel() ?: '' }}{{ number_format($p->sell_price, 2, '.', ' ') }}{{ number_format($p->total, 2, '.', ' ') }} + + {{ __(\App\Models\Tenant\WorkOrderPart::STATUSES[$p->status] ?? $p->status) }} + +
+ + @else +
+ {{ __('Nicio piesă montată.') }} + +
+ @endif +
+ + {{-- ── DIAGNOSTIC TAB ── --}} +
+
+
+

{{ __('Plângere client') }}

+
{{ $wo->complaint ?: '—' }}
+
+
+

{{ __('Diagnostic') }}

+
{{ $wo->diagnosis ?: '—' }}
+
+
+
+ + {{-- ── PHOTOS TAB ── --}} +
+ @php $photos = $wo->getMedia('photos'); @endphp + @if ($photos->isNotEmpty()) +
+ @foreach ($photos as $ph) + + + + @endforeach +
+ @else +
{{ __('Nicio fotografie') }}
+ @endif +
+ + {{-- ── DOCUMENTS TAB ── --}} +
+
{{ __('Nicio factură emisă încă') }}
+
+ + {{-- ── NOTES TAB ── --}} +
+
{{ $wo->notes ?: __('Nicio notă internă.') }}
+
+
+
+ + {{-- ═══ RIGHT COLUMN (placeholder — Phase 2) ═══ --}} +
+
+
+

{{ __('Finanțe') }}

+
+
+
+ {{ __('Cost manopere') }} + {{ number_format($finance['works_sum'], 2, '.', ' ') }} +
+
+ {{ __('Cost piese') }} + {{ number_format($finance['parts_sum'], 2, '.', ' ') }} +
+ @if ($finance['discount'] > 0) +
+ {{ __('Discount') }} + −{{ number_format($finance['discount'], 2, '.', ' ') }} +
+ @endif +
+ {{ __('Total') }} + {{ number_format($finance['total'], 2, '.', ' ') }} MDL +
+
+ {{ __('Achitat') }} + {{ number_format($finance['paid'], 2, '.', ' ') }} +
+ @if ($finance['balance'] > 0) +
+ {{ __('Rest de plată') }} + {{ number_format($finance['balance'], 2, '.', ' ') }} MDL +
+ @endif +
+
+
+ {{ __('Timeline & Chat — Phase 2') }} +
+
+
+
+