feat: rich Pipeline board — unified Lead/Deal/WO Kanban with SLA + drag-drop transitions
Replaces the bare 6-status WO Kanban with the unified Pipeline view from
/tmp/service/todo/psauto-pipeline-redesign.html. Six columns now span the
entire customer journey end-to-end:
Cerere nouă → Calculație → Programat → În lucru → Gata → Achitat azi
└─ Lead/Deal └─ Deal └─ Deal └─ WO └─ WO └─ WO+Payment
Cross-model drag-drop transitions:
- Lead → Calculație: Lead::convert() creates Deal at stage=contact, marks
quote_sent_at = now, quote_status = sent
- Deal (any earlier stage) → În lucru: spawns a WorkOrder from the deal
(client, vehicle, master, total, complaint), sets deal.stage=in_work,
links wo.deal_id
- WO → Gata: status=ready + fires NotificationDispatcher::workOrderReady
so client gets Telegram/email automatically
- WO → Achitat: creates Payment for remaining balance + status=done,
closed_at=today (pay_status syncs to paid via Payment booted hook)
Rich card content per the mockup:
- Red urgent stripe (left border) for Deal.urgent or WO.urgency!=normal
- Source tag (Instagram/Site/Apel/etc.) on lead/deal cards
- Quote status badge ("Trimis · fără răspuns" amber / "Văzut ✓" blue /
"A răspuns" green) based on deal.quote_status
- Scheduled time + bay tag ("05.06 · 09:00" + "Post 2")
- Fișă FL-NNN purple tag on WO cards
- "Necesită aprobare" amber tag when wo.status=agreement
- Progress bar (purple, 0-100%) on in-work cards: works_done + parts_installed
over total lines
- SLA time line per card with overdue red color:
* Lead 60+ min not contacted = overdue
* Quote 2h+ no response = overdue
* Ready 30+ min not paid = overdue (with phone icon)
* WO past ETA = overdue
- Assignee avatar (deterministic CRC32 color: blue/green/purple/amber)
- Amount in MDL, formatted
Stat strip (6 metrics computed live):
- Total deals active (sum of cols 1-5)
- MDL pipeline total
- MDL closed today (Payment sum where paid_at=today)
- Necesită acțiune (overdue + urgent + pending approval)
- Rata conversie 30d (won / (won+lost) %)
- Depășit termen (count WO past eta_at)
Filter chips wire-driven: Toate / Ale mele (assigned_to=me) /
Urgente (urgent=true OR wo.urgency!=normal) / Azi.
View toggle: Kanban ↔ Listă (table with all cards flat, sortable by stage).
Slide-in detail panel:
- 6-step stage stepper highlighting current
- Client / Telefon (blue clickable) / Auto / Sursă / Responsabil / Sumă /
De achitat (live computed balanceDue for WOs)
- Note / Reclamație
- Linked Fișă card with status badge, progress, ETA, "necesită aprobare"
alert + tracking link
- Activity timeline from Spatie activity-log
- Quick actions: WhatsApp (wa.me/<phone>), Sună (tel:), SMS (sms:),
Deschide (jumps to Filament resource edit)
DealResource hidden from nav (shouldRegisterNavigation=false) since
PipelineBoard is the canonical entry, but its edit/create routes stay
intact — the panel deep-links to them.
Auto-refresh: wire:poll.10s keeps the board live without WebSocket
dependency. Drag-drop is HTML5 native + Livewire wire:click for ops.
Dark mode supported via CSS variables overridden in .dark scope.
Migration: extend deals table with urgent, quote_sent_at, quote_status,
quote_seen_at, scheduled_at, bay, confirmed_at, confirmed_via,
last_action_at. Idempotent (hasColumn guards). Deal model auto-updates
last_action_at on saving.
Tests: 7 new + full suite 180/180 green (was 173).
- partition leads/deals/wos by column
- stats computation: active, pipeline_mdl, closed_today_mdl
- lead→quote transition converts lead into deal
- deal→in_work creates WorkOrder linked back to deal
- wo→paid creates payment for balance + marks done
- filter "mine" narrows to assigned user
- openCard loads panel detail with correct stepper position
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
<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>
|
||||
@@ -0,0 +1,398 @@
|
||||
<x-filament-panels::page>
|
||||
@php
|
||||
$columns = $this->getColumns();
|
||||
$stats = $this->getStats();
|
||||
$detail = $this->getOpenCardDetail();
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--pb-bg: #F7F7F5;
|
||||
--pb-surface: #FFFFFF;
|
||||
--pb-border: rgba(0,0,0,0.08);
|
||||
--pb-border-md: rgba(0,0,0,0.14);
|
||||
--pb-text: #111;
|
||||
--pb-text-2: #555;
|
||||
--pb-text-3: #999;
|
||||
--pb-blue: #1C6EF2;
|
||||
--pb-blue-bg: #EBF2FF;
|
||||
--pb-blue-text: #1347A8;
|
||||
--pb-green: #16A34A;
|
||||
--pb-green-bg: #DCFCE7;
|
||||
--pb-green-text: #166534;
|
||||
--pb-amber: #D97706;
|
||||
--pb-amber-bg: #FEF3C7;
|
||||
--pb-amber-text: #92400E;
|
||||
--pb-red: #DC2626;
|
||||
--pb-red-bg: #FEE2E2;
|
||||
--pb-red-text: #991B1B;
|
||||
--pb-purple: #7C3AED;
|
||||
--pb-purple-bg: #EDE9FE;
|
||||
--pb-purple-text: #4C1D95;
|
||||
--pb-gray-bg: #F1F0EE;
|
||||
--pb-gray-text: #444;
|
||||
}
|
||||
.dark {
|
||||
--pb-bg: #0f172a;
|
||||
--pb-surface: #1f2937;
|
||||
--pb-border: rgba(255,255,255,0.08);
|
||||
--pb-border-md: rgba(255,255,255,0.18);
|
||||
--pb-text: #f1f5f9;
|
||||
--pb-text-2: #cbd5e1;
|
||||
--pb-text-3: #94a3b8;
|
||||
--pb-blue-bg: #1e293b;
|
||||
--pb-blue-text: #93c5fd;
|
||||
--pb-green-bg: #14532d;
|
||||
--pb-green-text: #86efac;
|
||||
--pb-amber-bg: #422006;
|
||||
--pb-amber-text: #fcd34d;
|
||||
--pb-red-bg: #450a0a;
|
||||
--pb-red-text: #fca5a5;
|
||||
--pb-purple-bg: #3b0764;
|
||||
--pb-purple-text: #c4b5fd;
|
||||
--pb-gray-bg: #374151;
|
||||
--pb-gray-text: #d1d5db;
|
||||
}
|
||||
.pb-shell { background:var(--pb-bg); color:var(--pb-text); margin:-1.5rem; padding:0; min-height:calc(100vh - 80px); font-size:13px; }
|
||||
.pb-stat-strip { background:var(--pb-surface); border-bottom:1px solid var(--pb-border); padding:10px 20px; display:flex; gap:20px; flex-wrap:wrap; }
|
||||
.pb-stat-item { display:flex; flex-direction:column; gap:2px; min-width:60px; }
|
||||
.pb-stat-val { font-size:16px; font-weight:600; }
|
||||
.pb-stat-lbl { font-size:10px; color:var(--pb-text-3); white-space:nowrap; }
|
||||
.pb-stat-sep { width:1px; background:var(--pb-border); align-self:stretch; }
|
||||
|
||||
.pb-filter-bar { background:var(--pb-surface); border-bottom:1px solid var(--pb-border); padding:8px 20px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
||||
.pb-view-toggle { display:flex; background:var(--pb-bg); border-radius:6px; padding:2px; border:1px solid var(--pb-border); }
|
||||
.pb-vt-btn { display:flex; align-items:center; gap:4px; padding:4px 10px; border-radius:4px; font-size:11px; font-weight:500; cursor:pointer; color:var(--pb-text-3); }
|
||||
.pb-vt-btn.active { background:var(--pb-surface); color:var(--pb-text); box-shadow:0 1px 2px rgba(0,0,0,0.08); }
|
||||
.pb-chip { display:flex; align-items:center; gap:4px; padding:4px 10px; border-radius:20px; font-size:11px; font-weight:500; cursor:pointer; border:1px solid var(--pb-border); background:var(--pb-surface); color:var(--pb-text-2); }
|
||||
.pb-chip:hover { border-color:var(--pb-border-md); }
|
||||
.pb-chip.active { background:var(--pb-blue-bg); border-color:#93B8F9; color:var(--pb-blue-text); }
|
||||
.pb-filter-sep { width:1px; height:20px; background:var(--pb-border); }
|
||||
.pb-total { margin-left:auto; font-size:11px; color:var(--pb-text-2); }
|
||||
.pb-total strong { color:var(--pb-text); }
|
||||
|
||||
.pb-board { display:flex; gap:10px; overflow-x:auto; padding:16px 20px; align-items:flex-start; }
|
||||
.pb-col { width:240px; flex-shrink:0; display:flex; flex-direction:column; gap:6px; }
|
||||
.pb-col.over .pb-col-body { background:var(--pb-blue-bg); border-radius:8px; }
|
||||
.pb-col-head { background:var(--pb-surface); border:1px solid var(--pb-border); border-radius:10px; padding:10px 12px; }
|
||||
.pb-col-head-top { display:flex; align-items:center; justify-content:space-between; }
|
||||
.pb-col-name { font-size:12px; font-weight:600; display:flex; align-items:center; gap:6px; }
|
||||
.pb-col-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
||||
.pb-col-count { font-size:10px; font-weight:600; background:var(--pb-bg); padding:1px 6px; border-radius:8px; color:var(--pb-text-2); }
|
||||
.pb-col-sum { font-size:11px; color:var(--pb-text-3); margin-top:3px; }
|
||||
.pb-col-sum strong { color:var(--pb-text-2); }
|
||||
.pb-col-body { flex:1; display:flex; flex-direction:column; gap:6px; max-height:calc(100vh - 260px); overflow-y:auto; padding-bottom:4px; padding:2px; transition:background .15s; min-height:60px; }
|
||||
|
||||
.pb-deal { background:var(--pb-surface); border:1px solid var(--pb-border); border-radius:10px; padding:11px 12px; cursor:pointer; transition:all .12s; position:relative; }
|
||||
.pb-deal:hover { border-color:var(--pb-border-md); box-shadow:0 4px 12px rgba(0,0,0,0.1); transform:translateY(-1px); }
|
||||
.pb-deal.dragging { opacity:0.4; }
|
||||
.pb-deal-urgent { position:absolute; top:0; left:0; width:3px; height:100%; background:var(--pb-red); border-radius:10px 0 0 10px; }
|
||||
.pb-deal-id { font-size:10px; color:var(--pb-text-3); font-weight:500; letter-spacing:.3px; margin-bottom:4px; }
|
||||
.pb-deal-subject { font-size:12px; font-weight:600; margin-bottom:3px; line-height:1.3; }
|
||||
.pb-deal-car { font-size:11px; color:var(--pb-text-2); display:flex; align-items:center; gap:4px; margin-bottom:6px; }
|
||||
.pb-deal-meta { display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:8px; }
|
||||
.pb-tag { font-size:10px; font-weight:500; padding:2px 7px; border-radius:4px; white-space:nowrap; }
|
||||
.pb-tag-blue { background:var(--pb-blue-bg); color:var(--pb-blue-text); }
|
||||
.pb-tag-green { background:var(--pb-green-bg); color:var(--pb-green-text); }
|
||||
.pb-tag-amber { background:var(--pb-amber-bg); color:var(--pb-amber-text); }
|
||||
.pb-tag-red { background:var(--pb-red-bg); color:var(--pb-red-text); }
|
||||
.pb-tag-gray { background:var(--pb-gray-bg); color:var(--pb-gray-text); }
|
||||
.pb-tag-purple { background:var(--pb-purple-bg); color:var(--pb-purple-text); }
|
||||
.pb-deal-footer { display:flex; align-items:center; justify-content:space-between; }
|
||||
.pb-deal-assignee { display:flex; align-items:center; gap:5px; }
|
||||
.pb-av { width:20px; height:20px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:8px; font-weight:700; flex-shrink:0; }
|
||||
.pb-av-blue { background:var(--pb-blue-bg); color:var(--pb-blue-text); }
|
||||
.pb-av-green { background:var(--pb-green-bg); color:var(--pb-green-text); }
|
||||
.pb-av-purple { background:var(--pb-purple-bg); color:var(--pb-purple-text); }
|
||||
.pb-av-amber { background:var(--pb-amber-bg); color:var(--pb-amber-text); }
|
||||
.pb-av-gray { background:var(--pb-gray-bg); color:var(--pb-gray-text); }
|
||||
.pb-deal-name { font-size:11px; color:var(--pb-text-2); }
|
||||
.pb-deal-amount { font-size:12px; font-weight:600; }
|
||||
.pb-deal-time { font-size:10px; color:var(--pb-text-3); margin-top:5px; display:flex; align-items:center; gap:3px; }
|
||||
.pb-deal-time.overdue { color:var(--pb-red); }
|
||||
.pb-progress-bar { height:3px; background:var(--pb-bg); border-radius:2px; overflow:hidden; margin-top:6px; }
|
||||
.pb-progress-fill { height:100%; border-radius:2px; background:var(--pb-purple); }
|
||||
.pb-add-card { display:flex; align-items:center; gap:6px; padding:7px 12px; border-radius:10px; border:1px dashed var(--pb-border-md); color:var(--pb-text-3); font-size:11px; cursor:pointer; background:transparent; }
|
||||
.pb-add-card:hover { border-color:var(--pb-blue); color:var(--pb-blue); background:var(--pb-blue-bg); }
|
||||
|
||||
.pb-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.25); z-index:100; opacity:0; pointer-events:none; transition:opacity .2s; }
|
||||
.pb-overlay.open { opacity:1; pointer-events:all; }
|
||||
.pb-panel { position:fixed; right:0; top:0; bottom:0; width:420px; max-width:90vw; background:var(--pb-surface); border-left:1px solid var(--pb-border); z-index:101; transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1); overflow-y:auto; }
|
||||
.pb-panel.open { transform:translateX(0); }
|
||||
.pb-panel-head { padding:16px 20px 12px; border-bottom:1px solid var(--pb-border); display:flex; align-items:flex-start; justify-content:space-between; position:sticky; top:0; background:var(--pb-surface); z-index:10; }
|
||||
.pb-panel-title { font-size:15px; font-weight:600; line-height:1.3; }
|
||||
.pb-panel-id { font-size:11px; color:var(--pb-text-3); margin-top:2px; }
|
||||
.pb-close-btn { width:28px; height:28px; border-radius:6px; border:1px solid var(--pb-border); background:var(--pb-bg); cursor:pointer; display:flex; align-items:center; justify-content:center; }
|
||||
.pb-panel-body { padding:16px 20px; }
|
||||
.pb-pfield { margin-bottom:14px; }
|
||||
.pb-pfield-label { font-size:10px; font-weight:600; color:var(--pb-text-3); text-transform:uppercase; letter-spacing:.5px; margin-bottom:5px; }
|
||||
.pb-pfield-val { font-size:13px; font-weight:500; }
|
||||
.pb-two-cols { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||
.pb-panel-section { margin-top:16px; padding-top:16px; border-top:1px solid var(--pb-border); }
|
||||
.pb-panel-sec-title { font-size:11px; font-weight:600; color:var(--pb-text-3); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; }
|
||||
.pb-stage-stepper { display:flex; gap:4px; }
|
||||
.pb-stage-step { flex:1; height:4px; border-radius:2px; background:var(--pb-bg); }
|
||||
.pb-stage-step.done { background:var(--pb-green); }
|
||||
.pb-stage-step.current { background:var(--pb-blue); }
|
||||
.pb-stage-labels { display:flex; gap:4px; margin-top:5px; }
|
||||
.pb-stage-lbl { flex:1; font-size:9px; color:var(--pb-text-3); text-align:center; }
|
||||
.pb-stage-lbl.current { color:var(--pb-blue); font-weight:600; }
|
||||
.pb-activity-item { display:flex; gap:10px; margin-bottom:10px; }
|
||||
.pb-act-icon { width:24px; height:24px; border-radius:6px; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:12px; }
|
||||
.pb-act-text { font-size:12px; color:var(--pb-text-2); flex:1; }
|
||||
.pb-act-time { font-size:10px; color:var(--pb-text-3); margin-top:1px; }
|
||||
.pb-quick-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; }
|
||||
.pb-quick-btn { display:flex; align-items:center; justify-content:center; gap:5px; padding:7px; font-size:11px; border:1px solid var(--pb-border-md); border-radius:6px; background:var(--pb-surface); color:var(--pb-text); cursor:pointer; }
|
||||
.pb-quick-btn:hover { background:var(--pb-bg); }
|
||||
|
||||
.pb-empty-col { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:24px 12px; color:var(--pb-text-3); font-size:11px; }
|
||||
</style>
|
||||
|
||||
<div class="pb-shell" x-data="{ view: 'kanban', dragKey: null }">
|
||||
{{-- STAT STRIP --}}
|
||||
<div class="pb-stat-strip">
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val">{{ $stats['active'] }}</span>
|
||||
<span class="pb-stat-lbl">Total deals active</span>
|
||||
</div>
|
||||
<div class="pb-stat-sep"></div>
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val" style="color:var(--pb-blue)">{{ number_format($stats['pipeline_mdl'], 0, '.', ' ') }}</span>
|
||||
<span class="pb-stat-lbl">MDL pipeline total</span>
|
||||
</div>
|
||||
<div class="pb-stat-sep"></div>
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val" style="color:var(--pb-green)">{{ number_format($stats['closed_today_mdl'], 0, '.', ' ') }}</span>
|
||||
<span class="pb-stat-lbl">MDL închise azi</span>
|
||||
</div>
|
||||
<div class="pb-stat-sep"></div>
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val" style="color:var(--pb-amber)">{{ $stats['need_action'] }}</span>
|
||||
<span class="pb-stat-lbl">Necesită acțiune</span>
|
||||
</div>
|
||||
<div class="pb-stat-sep"></div>
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val">{{ $stats['conversion_rate'] }}%</span>
|
||||
<span class="pb-stat-lbl">Rata conversie (30z)</span>
|
||||
</div>
|
||||
<div class="pb-stat-sep"></div>
|
||||
<div class="pb-stat-item">
|
||||
<span class="pb-stat-val" style="color:var(--pb-red)">{{ $stats['overdue'] }}</span>
|
||||
<span class="pb-stat-lbl">Depășit termen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FILTER BAR --}}
|
||||
<div class="pb-filter-bar">
|
||||
<div class="pb-view-toggle">
|
||||
<div class="pb-vt-btn" :class="view==='kanban' && 'active'" @click="view='kanban'">📋 Kanban</div>
|
||||
<div class="pb-vt-btn" :class="view==='list' && 'active'" @click="view='list'">≡ Listă</div>
|
||||
</div>
|
||||
<div class="pb-filter-sep"></div>
|
||||
<div class="pb-chip {{ $activeFilter === 'all' ? 'active' : '' }}" wire:click="setFilter('all')">Toate</div>
|
||||
<div class="pb-chip {{ $activeFilter === 'mine' ? 'active' : '' }}" wire:click="setFilter('mine')">👤 Ale mele</div>
|
||||
<div class="pb-chip {{ $activeFilter === 'urgent' ? 'active' : '' }}" wire:click="setFilter('urgent')" style="color:var(--pb-red-text)">⚠ Urgente</div>
|
||||
<div class="pb-chip {{ $activeFilter === 'today' ? 'active' : '' }}" wire:click="setFilter('today')">📅 Azi</div>
|
||||
<div class="pb-total">Pipeline: <strong>{{ number_format($stats['pipeline_mdl'], 0, '.', ' ') }} MDL</strong> · <strong>{{ $stats['active'] }} deals</strong></div>
|
||||
</div>
|
||||
|
||||
{{-- KANBAN --}}
|
||||
<div class="pb-board" x-show="view==='kanban'" wire:poll.10s>
|
||||
@foreach ($columns as $colKey => $col)
|
||||
<div class="pb-col"
|
||||
@dragover.prevent="$el.classList.add('over')"
|
||||
@dragleave="$el.classList.remove('over')"
|
||||
@drop.prevent="
|
||||
$el.classList.remove('over');
|
||||
if (dragKey) {
|
||||
$wire.moveCard(dragKey, '{{ $colKey }}');
|
||||
dragKey = null;
|
||||
}
|
||||
">
|
||||
<div class="pb-col-head">
|
||||
<div class="pb-col-head-top">
|
||||
<div class="pb-col-name">
|
||||
<div class="pb-col-dot" style="background:{{ $col['color'] }}"></div>
|
||||
{{ $col['label'] }}
|
||||
</div>
|
||||
<span class="pb-col-count">{{ $col['count'] }}</span>
|
||||
</div>
|
||||
<div class="pb-col-sum"><strong>{{ number_format($col['sum'], 0, '.', ' ') }} MDL</strong></div>
|
||||
</div>
|
||||
<div class="pb-col-body">
|
||||
@forelse ($col['cards'] as $card)
|
||||
<div class="pb-deal"
|
||||
draggable="true"
|
||||
wire:click="openCard('{{ $card['key'] }}')"
|
||||
@dragstart="dragKey='{{ $card['key'] }}'; $el.classList.add('dragging')"
|
||||
@dragend="$el.classList.remove('dragging')">
|
||||
@if ($card['urgent'])
|
||||
<div class="pb-deal-urgent"></div>
|
||||
@endif
|
||||
<div class="pb-deal-id">{{ $card['code'] }}</div>
|
||||
<div class="pb-deal-subject">{{ $card['subject'] }}</div>
|
||||
<div class="pb-deal-car">🚗 {{ $card['plate'] }} · {{ $card['client_name'] }}</div>
|
||||
@if (!empty($card['tags']))
|
||||
<div class="pb-deal-meta">
|
||||
@foreach ($card['tags'] as $tag)
|
||||
<span class="pb-tag pb-tag-{{ $tag['color'] }}">{{ $tag['label'] }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div class="pb-deal-footer">
|
||||
<div class="pb-deal-assignee">
|
||||
<div class="pb-av pb-av-{{ $card['assignee']['color'] }}">{{ $card['assignee']['initials'] }}</div>
|
||||
<span class="pb-deal-name">{{ $card['assignee']['name'] }}</span>
|
||||
</div>
|
||||
<span class="pb-deal-amount">{{ number_format($card['amount'], 0, '.', ' ') }} MDL</span>
|
||||
</div>
|
||||
@if (!is_null($card['progress_pct']))
|
||||
<div class="pb-progress-bar"><div class="pb-progress-fill" style="width:{{ $card['progress_pct'] }}%"></div></div>
|
||||
@endif
|
||||
@if ($card['time_text'])
|
||||
<div class="pb-deal-time {{ $card['time_overdue'] ? 'overdue' : '' }}">
|
||||
@if ($card['time_icon']==='check')✓@elseif($card['time_icon']==='phone')📞@elseif($card['time_icon']==='message')💬@else⏱@endif
|
||||
{{ $card['time_text'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="pb-empty-col">Gol</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- LIST VIEW --}}
|
||||
<div x-show="view==='list'" style="padding:16px 20px;" x-cloak>
|
||||
<table style="width:100%; border-collapse:collapse; background:var(--pb-surface); border:1px solid var(--pb-border); border-radius:10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach (['#', 'Subiect', 'Client', 'Auto', 'Etapă', 'Sumă', 'Responsabil', 'Stare timp'] as $h)
|
||||
<th style="text-align:left; padding:8px 12px; font-size:10px; font-weight:600; color:var(--pb-text-3); text-transform:uppercase; border-bottom:1px solid var(--pb-border);">{{ $h }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($columns as $colKey => $col)
|
||||
@foreach ($col['cards'] as $card)
|
||||
<tr wire:click="openCard('{{ $card['key'] }}')" style="cursor:pointer; border-bottom:1px solid var(--pb-border);">
|
||||
<td style="padding:10px 12px; font-size:12px; color:var(--pb-text-3);">{{ $card['code'] }}</td>
|
||||
<td style="padding:10px 12px; font-size:13px; font-weight:500;">{{ $card['subject'] }}</td>
|
||||
<td style="padding:10px 12px; font-size:12px;">{{ $card['client_name'] }}</td>
|
||||
<td style="padding:10px 12px; font-size:12px;">{{ $card['plate'] }}</td>
|
||||
<td style="padding:10px 12px;"><span class="pb-tag pb-tag-gray">{{ $col['label'] }}</span></td>
|
||||
<td style="padding:10px 12px; font-size:12px; font-weight:600;">{{ number_format($card['amount'], 0, '.', ' ') }} MDL</td>
|
||||
<td style="padding:10px 12px; font-size:12px;">{{ $card['assignee']['name'] }}</td>
|
||||
<td style="padding:10px 12px; font-size:11px; color:{{ $card['time_overdue'] ? 'var(--pb-red)' : 'var(--pb-text-3)' }};">{{ $card['time_text'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- DETAIL PANEL --}}
|
||||
<div class="pb-overlay {{ $openCardKey ? 'open' : '' }}" wire:click="closeCard()"></div>
|
||||
<div class="pb-panel {{ $openCardKey ? 'open' : '' }}">
|
||||
@if ($detail)
|
||||
<div class="pb-panel-head">
|
||||
<div>
|
||||
<div class="pb-panel-title">{{ $detail['title'] }}</div>
|
||||
<div class="pb-panel-id">{{ $detail['subtitle'] }}</div>
|
||||
</div>
|
||||
<div class="pb-close-btn" wire:click="closeCard()">✕</div>
|
||||
</div>
|
||||
<div class="pb-panel-body">
|
||||
<div class="pb-panel-section" style="margin-top:0; padding-top:0; border-top:none;">
|
||||
<div class="pb-panel-sec-title">Progres etapă</div>
|
||||
<div class="pb-stage-stepper">
|
||||
@foreach ($detail['stages'] as $st)
|
||||
<div class="pb-stage-step {{ $st['done'] ? 'done' : '' }} {{ $st['current'] ? 'current' : '' }}"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="pb-stage-labels">
|
||||
@foreach ($detail['stages'] as $st)
|
||||
<div class="pb-stage-lbl {{ $st['current'] ? 'current' : '' }}">{{ $st['label'] }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-panel-section">
|
||||
<div class="pb-two-cols">
|
||||
@foreach ($detail['fields'] as $label => $value)
|
||||
@if ($value)
|
||||
<div class="pb-pfield">
|
||||
<div class="pb-pfield-label">{{ $label }}</div>
|
||||
<div class="pb-pfield-val" @if($label==='Telefon') style="color:var(--pb-blue)" @endif>{{ $value }}</div>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($detail['note'])
|
||||
<div class="pb-pfield">
|
||||
<div class="pb-pfield-label">Notițe / Reclamație</div>
|
||||
<div style="font-size:12px; color:var(--pb-text-2); white-space:pre-wrap;">{{ $detail['note'] }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (!empty($detail['wo']))
|
||||
<div class="pb-panel-section">
|
||||
<div class="pb-panel-sec-title">Fișă de lucru</div>
|
||||
<div style="background:var(--pb-purple-bg); border:1px solid #DDD6FE; border-radius:6px; padding:10px 12px; display:flex; align-items:center; justify-content:space-between;">
|
||||
<div>
|
||||
<div style="font-size:12px; font-weight:600; color:var(--pb-purple-text);">{{ $detail['wo']['number'] }} · {{ $detail['wo']['status_label'] }}</div>
|
||||
<div style="font-size:11px; color:var(--pb-purple-text); opacity:.8; margin-top:2px;">
|
||||
@if (!is_null($detail['wo']['progress_pct'])){{ $detail['wo']['progress_pct'] }}% finalizat @endif
|
||||
@if ($detail['wo']['eta']) · ETA {{ $detail['wo']['eta'] }} @endif
|
||||
</div>
|
||||
</div>
|
||||
<a class="pb-quick-btn" style="font-size:11px;" href="{{ $detail['edit_url'] }}">↗ Deschide</a>
|
||||
</div>
|
||||
@if (!empty($detail['wo']['has_pending_approval']))
|
||||
<div style="margin-top:8px; background:var(--pb-amber-bg); border:1px solid #FDE68A; border-radius:6px; padding:8px 12px;">
|
||||
<div style="font-size:11px; font-weight:600; color:var(--pb-amber-text);">⚠ Necesită aprobare client</div>
|
||||
<div style="font-size:11px; color:var(--pb-amber-text); margin-top:2px;">Deschide fișa pentru aprobare lucrare/piesă.</div>
|
||||
</div>
|
||||
@endif
|
||||
@if (!empty($detail['wo']['tracking_url']))
|
||||
<div style="margin-top:6px; font-size:11px;">
|
||||
Link tracking client: <a href="{{ $detail['wo']['tracking_url'] }}" target="_blank" style="color:var(--pb-blue);">{{ $detail['wo']['tracking_url'] }}</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!empty($detail['activity']))
|
||||
<div class="pb-panel-section">
|
||||
<div class="pb-panel-sec-title">Activitate recentă</div>
|
||||
@foreach ($detail['activity'] as $a)
|
||||
<div class="pb-activity-item">
|
||||
<div class="pb-act-icon" style="background:var(--pb-blue-bg); color:var(--pb-blue-text);">●</div>
|
||||
<div>
|
||||
<div class="pb-act-text">{{ $a['text'] }}</div>
|
||||
<div class="pb-act-time">{{ $a['time'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!empty($detail['phone']))
|
||||
<div class="pb-panel-section">
|
||||
<div class="pb-panel-sec-title">Acțiuni rapide</div>
|
||||
<div class="pb-quick-grid">
|
||||
<a class="pb-quick-btn" target="_blank" href="https://wa.me/{{ preg_replace('/\D/', '', $detail['phone']) }}">💚 WhatsApp</a>
|
||||
<a class="pb-quick-btn" href="tel:{{ $detail['phone'] }}">📞 Sună</a>
|
||||
<a class="pb-quick-btn" href="sms:{{ $detail['phone'] }}">💬 SMS</a>
|
||||
<a class="pb-quick-btn" href="{{ $detail['edit_url'] }}">↗ Deschide</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
Reference in New Issue
Block a user