Files
autocrm/app/Filament/Tenant/Pages/WorkOrderDashboard.php
T
Vasyka c33e96caf8 fix(work-order-dashboard): use Livewire's $this->redirect() in mount
Livewire's Redirector doesn't have ->send(); the correct API inside a
Livewire mount() is $this->redirect($url).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-05 19:44:08 +00:00

276 lines
9.2 KiB
PHP

<?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 = null;
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
{
$id = (int) $record;
$wo = WorkOrder::with([
'client', 'vehicle', 'master',
'works.labor', 'works.master',
'parts.part',
'payments.user',
'subcontractJobs.subcontractor',
])->find($id);
if (! $wo) {
// Diagnose: does it exist in another tenant?
$existsGlobally = WorkOrder::withoutGlobalScopes()
->where('id', $id)->exists();
$tenant = app(\App\Tenancy\TenantManager::class)->current();
$tenantSlug = $tenant?->slug ?? '—';
$msg = $existsGlobally
? sprintf(__('Fișa #%d există, dar nu aparține tenantului „%s".'), $id, $tenantSlug)
: sprintf(__('Fișa #%d nu există. Verifică lista de fișe active.'), $id);
\Filament\Notifications\Notification::make()
->title(__('Fișă indisponibilă'))
->body($msg)
->danger()
->persistent()
->send();
$this->redirect('/app/work-orders');
return;
}
$this->record = $wo;
}
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']);
}
/**
* 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
{
$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),
];
}
}