Files
autocrm/app/Filament/Tenant/Pages/WorkOrderDashboard.php
T
Vasyka bd8e42d3d1 diag(work-order-dashboard): log every mount + include URL in error
Adds Log::warning() on every mount() so we can trace exactly what
record id and URL the button navigates to. Also includes the full URL
in the "not found" alert so the user can copy it back to us.

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

287 lines
9.7 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;
// Log every single mount attempt so we can diagnose the exact request
\Log::warning('WorkOrderDashboard.mount', [
'record_raw' => $record,
'record_int' => $id,
'url' => request()->fullUrl(),
'referer' => request()->headers->get('referer'),
'method' => request()->method(),
'auth_id' => auth()->id(),
'tenant' => app(\App\Tenancy\TenantManager::class)->current()?->slug,
]);
$wo = WorkOrder::with([
'client', 'vehicle', 'master',
'works.labor', 'works.master',
'parts.part',
'payments.user',
'subcontractJobs.subcontractor',
])->find($id);
if (! $wo) {
$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ă. URL apelat: %s'), $id, request()->fullUrl());
\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),
];
}
}