diff --git a/app/Filament/Tenant/Pages/WorkOrderDashboard.php b/app/Filament/Tenant/Pages/WorkOrderDashboard.php index 907d1dc..e7f7a67 100644 --- a/app/Filament/Tenant/Pages/WorkOrderDashboard.php +++ b/app/Filament/Tenant/Pages/WorkOrderDashboard.php @@ -69,6 +69,168 @@ class WorkOrderDashboard extends Page ->get(['id', 'number', 'opened_at', 'total', 'status']); } + /** + * Timeline events for the right panel. Synthesises events from + * WO fields (created, opened, closed, paid, status transitions + * captured via activity_log if the trait is added) + notifications sent. + */ + public function getTimeline(): array + { + $wo = $this->record; + $events = []; + + if ($wo->created_at) { + $events[] = [ + 'at' => $wo->created_at, + 'kind' => 'created', + 'label' => __('Fișă creată'), + 'meta' => $wo->number, + 'icon' => '📋', + ]; + } + if ($wo->opened_at && $wo->opened_at->ne($wo->created_at)) { + $events[] = [ + 'at' => $wo->opened_at, + 'kind' => 'opened', + 'label' => __('Auto primit în service'), + 'meta' => null, + 'icon' => '🚗', + ]; + } + if ($wo->approved_at) { + $events[] = [ + 'at' => $wo->approved_at, + 'kind' => 'approved', + 'label' => __('Aprobat de client'), + 'meta' => null, + 'icon' => '✓', + ]; + } + if ($wo->closed_at) { + $events[] = [ + 'at' => $wo->closed_at instanceof \Carbon\Carbon ? $wo->closed_at : \Carbon\Carbon::parse($wo->closed_at), + 'kind' => 'closed', + 'label' => __('Fișă închisă'), + 'meta' => null, + 'icon' => '🔒', + ]; + } + + // Payments + foreach ($wo->payments as $p) { + $events[] = [ + 'at' => $p->paid_at, + 'kind' => 'payment', + 'label' => __('Plată primită'), + 'meta' => number_format((float) $p->amount, 2, '.', ' ') . ' MDL · ' . __(\App\Models\Tenant\Payment::METHODS[$p->method] ?? $p->method), + 'icon' => '💰', + ]; + } + + // Activity log (spatie/activitylog) — best-effort read + try { + $activities = \DB::table('activity_log') + ->where('subject_type', WorkOrder::class) + ->where('subject_id', $wo->id) + ->where('company_id', $wo->company_id) + ->orderBy('created_at') + ->get(); + foreach ($activities as $a) { + $events[] = [ + 'at' => \Carbon\Carbon::parse($a->created_at), + 'kind' => $a->event ?? 'log', + 'label' => $a->description ?: ucfirst($a->event ?? 'log'), + 'meta' => null, + 'icon' => '📝', + ]; + } + } catch (\Throwable) { /* activity_log table may be empty on fresh DB */ } + + // Sort chronologically desc so newest first + usort($events, fn ($a, $b) => $b['at']->timestamp <=> $a['at']->timestamp); + return $events; + } + + /** Outbound notifications to the client (SMS/WhatsApp/Telegram/Email). */ + public function getNotifications(): \Illuminate\Support\Collection + { + return \App\Models\Tenant\ClientNotificationLog::where('work_order_id', $this->record->id) + ->orderByDesc('sent_at') + ->limit(20) + ->get(); + } + + public string $newMessage = ''; + + public function sendMessage(): void + { + $text = trim($this->newMessage); + if ($text === '' || ! $this->record->client) return; + + $client = $this->record->client; + $company = app(\App\Tenancy\TenantManager::class)->current(); + if (! $company) return; + + $dispatcher = app(\App\Services\NotificationDispatcher::class); + $sent = false; + + // Prefer Telegram if the client is linked; fall back to WhatsApp if a phone is set. + if (! empty($client->telegram_chat_id)) { + try { + $sent = app(\App\Services\Notifications\TelegramService::class) + ->sendMessage($company, (string) $client->telegram_chat_id, $text); + } catch (\Throwable) { /* ignore */ } + } + + // Log the message so it shows up in the chat pane regardless of delivery. + \App\Models\Tenant\ClientNotificationLog::create([ + 'company_id' => $company->id, + 'work_order_id' => $this->record->id, + 'client_id' => $client->id, + 'channel' => ! empty($client->telegram_chat_id) ? 'telegram' : 'sms', + 'template_key' => 'manual', + 'message_text' => $text, + 'status' => $sent ? 'sent' : 'failed', + 'sent_at' => now(), + ]); + + $this->newMessage = ''; + + \Filament\Notifications\Notification::make() + ->title($sent ? __('Mesaj trimis') : __('Trimitere eșuată — verifică integrarea')) + ->{$sent ? 'success' : 'warning'}() + ->send(); + } + + /** Adjacent WOs for the previous/next buttons in the bottom bar. */ + public function getPrevWo(): ?WorkOrder + { + return WorkOrder::where('id', '<', $this->record->id) + ->orderByDesc('id') + ->first(['id', 'number']); + } + + public function getNextWo(): ?WorkOrder + { + return WorkOrder::where('id', '>', $this->record->id) + ->orderBy('id') + ->first(['id', 'number']); + } + + public function closeWorkOrder(): void + { + $wo = $this->record; + if ($wo->status === 'done') return; + $wo->forceFill([ + 'status' => 'done', + 'closed_at' => now(), + ])->save(); + $this->record->refresh(); + \Filament\Notifications\Notification::make() + ->title(__('Fișă închisă')) + ->success()->send(); + } + /** Aggregated finance data — used by right panel in future phases. */ public function getFinance(): array { diff --git a/lang/en.json b/lang/en.json index 48cd925..2966ea4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -263,6 +263,7 @@ "Auto": "Vehicle", "Auto / model": "Vehicle / model", "Auto CRM": "CRM vehicle", + "Auto primit în service": "Vehicle received", "Auto-import Excel/CSV": "Auto-import Excel/CSV", "AutoCRM PSauto · psauto.service.mir.md": "AutoCRM PSauto · psauto.service.mir.md", "AutoCRM SRL": "AutoCRM SRL", @@ -383,6 +384,7 @@ "Cereri": "Leads", "Cereri noi": "New leads", "Channel": "Channel", + "Chat client": "Client chat", "Check-in depozit": "Check-in storage", "Checksum (ISO)": "Checksum (ISO)", "Cheia HMAC pentru semnarea cererilor + verificarea webhook-urilor de la Paynet.": "Cheia HMAC pentru semnarea cererilor + verificarea webhook-urilor de la Paynet.", @@ -455,6 +457,7 @@ "Comandă emisă": "Order issued", "Comandă nouă #": "New order #", "Comandă primită": "Order received", + "Comandă repetată": "Repeat order", "Combustibil": "Fuel", "Comentariu / recomandări maistru": "Master comment / recommendations", "Comenzi": "Orders", @@ -816,6 +819,7 @@ "Extras": "Statement", "Eșapament": "Exhaust", "Ești sigur?": "Are you sure?", + "Eșuat": "Failed", "FIȘĂ DE LUCRU": "WORK ORDER", "Facturi": "Invoices", "Facturi & abonament": "Facturi & abonament", @@ -855,9 +859,11 @@ "Fișierul nu există.": "File does not exist.", "Fișă": "Order", "Fișă asociată": "Linked order", + "Fișă creată": "Order created", "Fișă de lucru": "Work order", "Fișă lucru": "Work order", "Fișă lucru (opțional)": "Work order (opt.)", + "Fișă închisă": "Order closed", "Folie PPF": "PPF film", "Folosește AI Assistant": "Use AI Assistant", "Folosește „Check-in depozit": "Use \"Check-in storage\"", @@ -892,6 +898,8 @@ "Fără acest cod, plata va fi greu de identificat.": "Without this code, payment will be hard to identify.", "Fără articol (manual)": "No article (manual)", "Fără client": "No client", + "Fără client — nu se poate trimite mesaj.": "No client — cannot send message.", + "Fără evenimente": "No events", "Fără maistru": "No master", "Fără mașină": "No vehicle", "Fără răspuns": "No answer", @@ -1168,6 +1176,7 @@ "Merchant Code": "Merchant Code", "Mesaj": "Message", "Mesaj client": "Client message", + "Mesaj trimis": "Message sent", "Mesaje": "Messages", "Mesaje AI/lună": "AI messages/month", "Message": "Message", @@ -1496,6 +1505,7 @@ "Plată": "Payment", "Plată & total": "Payment & total", "Plată anulată": "Plată anulată", + "Plată primită": "Payment received", "Plată reușită": "Plată reușită", "Plată reușită!": "Plată reușită!", "Plângere client": "Client complaint", @@ -1987,6 +1997,7 @@ "Testează bot Telegram": "Test Telegram bot", "Theme color": "Theme color", "Time": "Time", + "Timeline": "Timeline", "Timeline & Chat — Phase 2": "Timeline & Chat — Phase 2", "Timp deschidere, ms": "Opening time, ms", "Timp lucrat": "Time worked", @@ -2083,6 +2094,7 @@ "Trimite prin email": "Send by email", "Trimite reminder după X zile fără vizită": "Send reminder after X days without visit", "Trimite reset parolă": "Send password reset", + "Trimitere eșuată — verifică integrarea": "Send failed — check integration", "Turbo": "Turbo", "Type": "Type", "UAH — Hryvnia": "UAH — Hryvnia", @@ -2517,6 +2529,8 @@ "Începe lucrul": "Start work", "Închide": "Close", "Închide fișa": "Close order", + "Închide fișa?": "Close the order?", + "Închide fișă": "Close order", "Închidere KZ ÎNAINTE": "Closing KZ BEFORE", "Închidere ÎNAINTE": "Closing BEFORE", "Închis": "Closed", diff --git a/lang/ru.json b/lang/ru.json index 5add77a..b079611 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -263,6 +263,7 @@ "Auto": "Авто", "Auto / model": "Авто / модель", "Auto CRM": "CRM-авто", + "Auto primit în service": "Авто принято в сервис", "Auto-import Excel/CSV": "Auto-import Excel/CSV", "AutoCRM PSauto · psauto.service.mir.md": "AutoCRM PSauto · psauto.service.mir.md", "AutoCRM SRL": "AutoCRM SRL", @@ -383,6 +384,7 @@ "Cereri": "Заявки", "Cereri noi": "Новые заявки", "Channel": "Канал", + "Chat client": "Чат с клиентом", "Check-in depozit": "Приёмка на склад", "Checksum (ISO)": "Checksum (ISO)", "Cheia HMAC pentru semnarea cererilor + verificarea webhook-urilor de la Paynet.": "Cheia HMAC pentru semnarea cererilor + verificarea webhook-urilor de la Paynet.", @@ -455,6 +457,7 @@ "Comandă emisă": "Заказ создан", "Comandă nouă #": "Новый заказ #", "Comandă primită": "Заказ получен", + "Comandă repetată": "Повторный заказ", "Combustibil": "Топливо", "Comentariu / recomandări maistru": "Комментарий / рекомендации мастера", "Comenzi": "Заказы", @@ -816,6 +819,7 @@ "Extras": "Выписка", "Eșapament": "Выхлоп", "Ești sigur?": "Ты уверен?", + "Eșuat": "Ошибка", "FIȘĂ DE LUCRU": "ЗАКАЗ-НАРЯД", "Facturi": "Счета", "Facturi & abonament": "Facturi & abonament", @@ -855,9 +859,11 @@ "Fișierul nu există.": "Файл не существует.", "Fișă": "Наряд", "Fișă asociată": "Связанный наряд", + "Fișă creată": "Наряд создан", "Fișă de lucru": "Заказ-наряд", "Fișă lucru": "Заказ-наряд", "Fișă lucru (opțional)": "Заказ-наряд (опц.)", + "Fișă închisă": "Наряд закрыт", "Folie PPF": "Плёнка PPF", "Folosește AI Assistant": "Использовать AI Assistant", "Folosește „Check-in depozit": "Используйте «Приёмка на склад»", @@ -892,6 +898,8 @@ "Fără acest cod, plata va fi greu de identificat.": "Без этого кода платёж трудно идентифицировать.", "Fără articol (manual)": "Без артикула (вручную)", "Fără client": "Без клиента", + "Fără client — nu se poate trimite mesaj.": "Нет клиента — сообщение отправить нельзя.", + "Fără evenimente": "Нет событий", "Fără maistru": "Без мастера", "Fără mașină": "Без авто", "Fără răspuns": "Без ответа", @@ -1168,6 +1176,7 @@ "Merchant Code": "Merchant Code", "Mesaj": "Сообщение", "Mesaj client": "Сообщение клиента", + "Mesaj trimis": "Сообщение отправлено", "Mesaje": "Сообщения", "Mesaje AI/lună": "AI-сообщения/мес", "Message": "Сообщение", @@ -1496,6 +1505,7 @@ "Plată": "Платёж", "Plată & total": "Оплата и итого", "Plată anulată": "Plată anulată", + "Plată primită": "Платёж получен", "Plată reușită": "Plată reușită", "Plată reușită!": "Plată reușită!", "Plângere client": "Жалоба клиента", @@ -1987,6 +1997,7 @@ "Testează bot Telegram": "Тест Telegram-бота", "Theme color": "Theme color", "Time": "Время", + "Timeline": "Хронология", "Timeline & Chat — Phase 2": "Timeline и чат — этап 2", "Timp deschidere, ms": "Время открытия, мс", "Timp lucrat": "Отработано", @@ -2083,6 +2094,7 @@ "Trimite prin email": "Отправить по email", "Trimite reminder după X zile fără vizită": "Отправить напоминание через X дней без визита", "Trimite reset parolă": "Отправить сброс пароля", + "Trimitere eșuată — verifică integrarea": "Ошибка отправки — проверьте интеграцию", "Turbo": "Турбина", "Type": "Тип", "UAH — Hryvnia": "UAH — Гривна", @@ -2517,6 +2529,8 @@ "Începe lucrul": "Начать работу", "Închide": "Закрыть", "Închide fișa": "Закрыть наряд", + "Închide fișa?": "Закрыть наряд?", + "Închide fișă": "Закрыть наряд", "Închidere KZ ÎNAINTE": "Закрытие КЗ ДО", "Închidere ÎNAINTE": "Закрытие ДО", "Închis": "Закрыт", diff --git a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php index de86dc5..e3bfe07 100644 --- a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php +++ b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php @@ -4,6 +4,10 @@ $wo = $this->record; $finance = $this->getFinance(); $history = $this->getRepairHistory(); + $timeline = $this->getTimeline(); + $messages = $this->getNotifications(); + $prev = $this->getPrevWo(); + $next = $this->getNextWo(); $tabs = [ 'works' => ['label' => __('Lucrări'), 'count' => $wo->works->count()], 'parts' => ['label' => __('Piese'), 'count' => $wo->parts->count()], @@ -325,14 +329,50 @@ } .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; +/* ── TIMELINE ── */ +.wd-timeline { display: flex; flex-direction: column; gap: 10px; max-height: 240px; overflow-y: auto; } +.wd-tl-item { display: flex; gap: 8px; font-size: 11px; } +.wd-tl-icon { + width: 24px; height: 24px; border-radius: 50%; + background: var(--wd-bg); border: 1px solid var(--wd-border); + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; font-size: 11px; } +.wd-tl-body { flex: 1; min-width: 0; } +.wd-tl-body .lbl { font-weight: 600; color: var(--wd-text); line-height: 1.3; } +.wd-tl-body .meta { color: var(--wd-text-3); font-size: 10px; margin-top: 2px; } + +/* ── CHAT ── */ +.wd-chat { display: flex; flex-direction: column; height: 260px; } +.wd-chat-list { flex: 1; overflow-y: auto; padding-right: 4px; } +.wd-msg { margin-bottom: 8px; } +.wd-msg-meta { font-size: 10px; color: var(--wd-text-3); margin-bottom: 2px; display: flex; gap: 6px; } +.wd-msg-body { + background: var(--wd-blue-bg); color: var(--wd-text); + padding: 6px 10px; border-radius: 8px 8px 8px 2px; + font-size: 11px; line-height: 1.4; + white-space: pre-wrap; +} +.wd-msg.failed .wd-msg-body { background: var(--wd-red-bg); color: var(--wd-red); } +.wd-chat-input { display: flex; gap: 6px; margin-top: 8px; } +.wd-chat-input input { + flex: 1; padding: 7px 10px; font-size: 12px; + border: 1px solid var(--wd-border-md); border-radius: 6px; + background: var(--wd-surface); color: var(--wd-text); +} + +/* ── BOTTOM ACTION BAR ── */ +.wd-bottom { + background: var(--wd-surface); + border-top: 1px solid var(--wd-border); + padding: 10px 20px; + display: flex; gap: 8px; align-items: center; + flex-shrink: 0; +} +.wd-bottom-nav { display: flex; gap: 4px; } +.wd-bottom-actions { margin-left: auto; display: flex; gap: 6px; } +.wd-btn-danger { background: var(--wd-red); color: #fff; border-color: var(--wd-red); } +.wd-btn-danger:hover { background: #991b1b; }