feat(work-order-dashboard): Phase 2 — Timeline + Chat + Bottom action bar

Right column now shows:
- Timeline card: synthesised events from WO lifecycle fields (created,
  opened, approved, closed) + payments + activity_log entries (spatie/
  activitylog). Sorted newest first, scrollable.
- Chat client card: outbound notifications history (ClientNotificationLog)
  + inline send form. Prefers Telegram if client has telegram_chat_id,
  else falls back to SMS/WhatsApp. Logged either way for UI history.

New bottom action bar (fixed, above footer):
- Previous / Next WO links (adjacent by id) with number preview
- Repeat order: pre-fills create form with same client/vehicle
- Close order (danger button): sets status='done' + closed_at=now, with
  wire:confirm guard. Hidden when already closed.

+14 translations. Fixed ready_at reference (not in schema, removed).
All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 19:00:28 +00:00
parent fa8704f8b6
commit 9830dd9a6f
4 changed files with 322 additions and 9 deletions
@@ -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; }
</style>
<div class="wd-shell" x-data="{ tab: @entangle('activeTab').live }">
@@ -686,9 +726,92 @@
@endif
</div>
</div>
<div class="wd-placeholder">
{{ __('Timeline & Chat — Phase 2') }}
{{-- TIMELINE --}}
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Timeline') }}</h3>
</div>
@if (! empty($timeline))
<div class="wd-timeline">
@foreach ($timeline as $ev)
<div class="wd-tl-item">
<div class="wd-tl-icon">{{ $ev['icon'] }}</div>
<div class="wd-tl-body">
<div class="lbl">{{ $ev['label'] }}</div>
<div class="meta">
{{ $ev['at']->format('d.m.Y H:i') }}
@if ($ev['meta']) · {{ $ev['meta'] }} @endif
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="wd-empty" style="padding:16px 0;">{{ __('Fără evenimente') }}</div>
@endif
</div>
{{-- CHAT CLIENT --}}
<div class="wd-card">
<div class="wd-card-hd">
<h3>{{ __('Chat client') }}</h3>
@if ($wo->client?->telegram_chat_id)
<span style="font-size:10px;color:var(--wd-blue);"> Telegram</span>
@elseif ($wo->client?->phone)
<span style="font-size:10px;color:var(--wd-text-3);">SMS/WhatsApp</span>
@endif
</div>
@if ($wo->client)
<div class="wd-chat">
<div class="wd-chat-list">
@forelse ($messages as $m)
<div class="wd-msg {{ $m->status === 'failed' ? 'failed' : '' }}">
<div class="wd-msg-meta">
<span>{{ strtoupper($m->channel) }}</span>
<span>{{ $m->sent_at?->format('d.m H:i') }}</span>
@if ($m->status === 'failed')<span style="color:var(--wd-red);"> {{ __('Eșuat') }}</span>@endif
</div>
<div class="wd-msg-body">{{ $m->message_text }}</div>
</div>
@empty
<div class="wd-empty" style="padding:16px 0;">{{ __('Nicio conversație') }}</div>
@endforelse
</div>
<form class="wd-chat-input" wire:submit.prevent="sendMessage">
<input type="text" placeholder="{{ __('Scrie mesajul tău...') }}" wire:model="newMessage" maxlength="1000">
<button type="submit" class="wd-btn wd-btn-primary"></button>
</form>
</div>
@else
<div class="wd-empty" style="padding:16px 0;">{{ __('Fără client — nu se poate trimite mesaj.') }}</div>
@endif
</div>
</div>
</div>
{{-- ── BOTTOM ACTION BAR ── --}}
<div class="wd-bottom">
<div class="wd-bottom-nav">
@if ($prev)
<a class="wd-btn" href="{{ route('filament.tenant.pages.work-order-dashboard', ['record' => $prev->id]) }}">
{{ __('Precedent') }} <span style="color:var(--wd-text-3);">({{ $prev->number }})</span>
</a>
@endif
@if ($next)
<a class="wd-btn" href="{{ route('filament.tenant.pages.work-order-dashboard', ['record' => $next->id]) }}">
{{ __('Următor') }} <span style="color:var(--wd-text-3);">({{ $next->number }})</span>
</a>
@endif
</div>
<div class="wd-bottom-actions">
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.create') }}?client_id={{ $wo->client_id }}&vehicle_id={{ $wo->vehicle_id }}">
📄 {{ __('Comandă repetată') }}
</a>
@if ($wo->status !== 'done')
<button type="button" class="wd-btn wd-btn-danger" wire:click="closeWorkOrder" wire:confirm="{{ __('Închide fișa?') }}">
🔒 {{ __('Închide fișă') }}
</button>
@endif
</div>
</div>
</div>