7ce78c350c
- laravel/reverb instalat + reverb:install (config/reverb.php, channels.php)
- routes/channels.php: tenant.{slug} private channel cu auth check
user.company_id == tenant.id
- App\Events\WorkOrderUpdated implements ShouldBroadcast pe
PrivateChannel('tenant.{slug}'); broadcastAs 'work-order.updated'
- WorkOrder::booted dispatch event la fiecare update (skip if broadcast=log)
- Filament panel BODY_END inject:
- Pusher JS de la CDN (compatibil Reverb)
- Echo client conectat la Reverb (config dinamic din env)
- Subscribe pe tenant private channel; la 'work-order.updated' →
Livewire.all().forEach($refresh)
- Kanban view: wire:poll.5s (live refresh fallback) +
x-on:autocrm:wo-updated.window=$refresh (instant când WS e activ)
Pentru moment BROADCAST_CONNECTION=log în Coolify (Reverb nu e deployat).
Când deployezi Reverb container separat:
Coolify → New App → Same repo → CMD override:
php artisan reverb:start --host=0.0.0.0 --port=8080
→ FQDN: ws.service.mir.md:8080
→ Set BROADCAST_CONNECTION=reverb pe AutoCRM app
→ Real-time instant fără cod nou.
93 lines
4.8 KiB
PHP
93 lines
4.8 KiB
PHP
<x-filament-panels::page>
|
|
@php $columns = $this->getColumns(); @endphp
|
|
|
|
<style>
|
|
.kb-board { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 1rem; }
|
|
.kb-col {
|
|
width: 290px; flex-shrink: 0; padding: 12px;
|
|
background: #f1f5f9; border-radius: 10px;
|
|
border: 2px solid transparent;
|
|
transition: border-color .15s;
|
|
}
|
|
.dark .kb-col { background: #1f2937; }
|
|
.kb-col.over { border-color: #3b82f6; }
|
|
.kb-col-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
.kb-col-title { font-size: 14px; font-weight: 600; }
|
|
.kb-col-count { font-size: 11px; padding: 2px 8px; background: #e2e8f0; border-radius: 999px; }
|
|
.dark .kb-col-count { background: #374151; }
|
|
.kb-list { display: flex; flex-direction: column; gap: 8px; min-height: 80px; }
|
|
.kb-card {
|
|
background: #fff; border: 1px solid #e5e7eb; border-radius: 8px;
|
|
padding: 12px; cursor: move;
|
|
transition: box-shadow .15s, opacity .15s;
|
|
}
|
|
.dark .kb-card { background: #111827; border-color: #374151; }
|
|
.kb-card:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.08); }
|
|
.kb-card.dragging { opacity: 0.4; }
|
|
.kb-num { font-size: 11px; font-family: monospace; color: #6b7280; }
|
|
.kb-title { font-size: 14px; font-weight: 600; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.kb-sub { font-size: 12px; color: #6b7280; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.kb-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; font-size: 11px; }
|
|
.kb-link { display: block; margin-top: 8px; font-size: 11px; color: #3b82f6; text-decoration: none; }
|
|
.kb-link:hover { text-decoration: underline; }
|
|
.kb-empty { font-size: 11px; color: #9ca3af; text-align: center; padding: 16px 0; }
|
|
</style>
|
|
|
|
{{-- Live: poll fallback every 5s + instant refresh on WebSocket event --}}
|
|
<div x-data="{ dragId: null }"
|
|
wire:poll.5s
|
|
x-on:autocrm:wo-updated.window="$wire.$refresh()">
|
|
<div class="kb-board">
|
|
@foreach ($columns as $status => $col)
|
|
<div
|
|
class="kb-col"
|
|
@dragover.prevent="$el.classList.add('over')"
|
|
@dragleave="$el.classList.remove('over')"
|
|
@drop.prevent="
|
|
$el.classList.remove('over');
|
|
if (dragId) {
|
|
$wire.moveCard(parseInt(dragId), '{{ $status }}');
|
|
dragId = null;
|
|
}
|
|
"
|
|
>
|
|
<div class="kb-col-head">
|
|
<span class="kb-col-title">{{ $col['label'] }}</span>
|
|
<span class="kb-col-count">{{ $col['count'] }}</span>
|
|
</div>
|
|
<div class="kb-list">
|
|
@forelse ($col['cards'] as $wo)
|
|
<div
|
|
class="kb-card"
|
|
draggable="true"
|
|
@dragstart="dragId = '{{ $wo->id }}'; $el.classList.add('dragging')"
|
|
@dragend="$el.classList.remove('dragging')"
|
|
>
|
|
<div class="kb-num">{{ $wo->number }}</div>
|
|
<div class="kb-title">{{ $wo->client?->name ?? '—' }}</div>
|
|
<div class="kb-sub">
|
|
{{ $wo->vehicle?->make }} {{ $wo->vehicle?->model }}
|
|
@if ($wo->vehicle?->plate)
|
|
[{{ $wo->vehicle->plate }}]
|
|
@endif
|
|
</div>
|
|
<div class="kb-foot">
|
|
<span style="color:#6b7280">{{ $wo->master?->name ?? '—' }}</span>
|
|
<span style="font-weight:600">{{ number_format((float)$wo->total, 0, '.', ' ') }} MDL</span>
|
|
</div>
|
|
<a class="kb-link" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id]) }}">Deschide →</a>
|
|
</div>
|
|
@empty
|
|
<div class="kb-empty">Gol</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div style="font-size: 11px; color: #6b7280; margin-top: 12px;">
|
|
💡 Drag-drop carduri între coloane pentru a schimba statusul.
|
|
</div>
|
|
</div>
|
|
</x-filament-panels::page>
|