feat: Pipeline board full-bleed + hover actions + Programare→Calendar

Addresses the gaps surfaced after the first redesign — the board was still
boxed in Filament chrome (not truly full-page), hover floating-actions and
"+ Adaugă" CTAs were missing, and the P0 "Programează" from deal card had
no calendar wiring.

Full-page:
- getMaxContentWidth() = Width::Full
- getHeading()/getSubheading() return empty so Filament's title bar
  disappears, leaving the kanban edge-to-edge
- CSS uses :has(.pb-shell) to strip Filament's page padding + heading
  block at the layout level
- Board height = calc(100vh - 64px); columns scroll independently

Hover floating-actions on every card (column-aware):
- Cols 1-2 (Cerere / Calculație): 📅 quickSchedule
- Col 3 (Programat): ▶ start work (creates WO)
- Col 4 (În lucru): ✓ mark Gata
- Col 5 (Gata): 💰 mark Achitat
- All cards with phone: 📞 tel: + 💬 wa.me
- All cards: ↗ open in resource edit
- Shown only on .pb-deal:hover, positioned absolute top-right

"+ Adaugă" CTA at column bottom:
- Cols 1-3 → /app/leads/create
- Cols 4-5 → /app/work-orders/create

Programare → Calendar (P0 AAA):
- quickSchedule($key) on PipelineBoard creates a real Appointment row for
  tomorrow 10:00 linked to (client_id, vehicle_id, master_id, deal_id),
  sets deal.stage='scheduled' + scheduled_at, then shows a toast
- Panel bottom action bar gains "📅 Programează" CTA for lead/deal cards
- "📅 Calendar" jump CTA for WO cards
- calendarUrl() returns the canonical filament.tenant.pages.calendar-board
  route

Empty column state now reads "Gol — trage un card aici" instead of just
"Gol" so the drop affordance is explicit.

Stat strip + filter bar sticky at top; board fills the remaining viewport.

Tests: +1 (quickSchedule creates Appointment + moves deal). Suite 181/181.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:14:20 +00:00
parent 3603c0e43b
commit 3c0f3ba39e
3 changed files with 160 additions and 21 deletions
@@ -53,14 +53,22 @@
--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; }
/* Break out of Filament chrome — full-bleed board */
.fi-main-ctn:has(.pb-shell) { padding: 0 !important; }
.fi-main:has(.pb-shell) { padding: 0 !important; }
.fi-page:has(.pb-shell) > div { padding: 0 !important; gap: 0 !important; }
.fi-page:has(.pb-shell) .fi-header { display: none !important; }
.fi-page:has(.pb-shell) { gap: 0 !important; }
.pb-shell { background:var(--pb-bg); color:var(--pb-text); margin:0; padding:0; min-height:calc(100vh - 64px); font-size:13px; display:flex; flex-direction:column; }
.pb-stat-strip { background:var(--pb-surface); border-bottom:1px solid var(--pb-border); padding:10px 20px; display:flex; gap:20px; flex-wrap:wrap; flex-shrink:0; }
.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-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; flex-shrink:0; }
.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); }
@@ -71,17 +79,19 @@
.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-board { display:flex; gap:10px; overflow-x:auto; overflow-y:hidden; padding:16px 20px; align-items:flex-start; flex:1; min-height:0; }
.pb-col { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:6px; height:100%; }
.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 { background:var(--pb-surface); border:1px solid var(--pb-border); border-radius:10px; padding:10px 12px; flex-shrink:0; }
.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-col-body { flex:1; display:flex; flex-direction:column; gap:6px; overflow-y:auto; padding:2px; transition:background .15s; min-height:60px; }
.pb-col-body::-webkit-scrollbar { width:3px; }
.pb-col-body::-webkit-scrollbar-thumb { background:var(--pb-border-md); border-radius:2px; }
.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); }
@@ -112,18 +122,25 @@
.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; }
/* Hover floating action buttons */
.pb-deal-actions { position:absolute; top:8px; right:8px; display:none; gap:3px; z-index:2; }
.pb-deal:hover .pb-deal-actions { display:flex; }
.pb-act-btn { width:24px; height:24px; border-radius:5px; border:1px solid var(--pb-border); background:var(--pb-surface); cursor:pointer; display:flex; align-items:center; justify-content:center; color:var(--pb-text-2); font-size:13px; text-decoration:none; }
.pb-act-btn:hover { background:var(--pb-bg); color:var(--pb-text); border-color:var(--pb-border-md); }
.pb-add-card { display:flex; align-items:center; justify-content:center; gap:6px; padding:8px 12px; border-radius:10px; border:1px dashed var(--pb-border-md); color:var(--pb-text-3); font-size:11px; cursor:pointer; background:transparent; text-decoration:none; flex-shrink:0; }
.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 { position:fixed; right:0; top:0; bottom:0; width:440px; max-width:92vw; 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; display:flex; flex-direction:column; }
.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-panel-body { padding:16px 20px; flex:1; overflow-y:auto; }
.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; }
@@ -142,13 +159,18 @@
.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 { display:flex; align-items:center; justify-content:center; gap:5px; padding:8px; font-size:11px; border:1px solid var(--pb-border-md); border-radius:6px; background:var(--pb-surface); color:var(--pb-text); cursor:pointer; text-decoration:none; }
.pb-quick-btn:hover { background:var(--pb-bg); }
.pb-quick-btn.primary { background:var(--pb-blue); color:#fff; border-color:var(--pb-blue); }
.pb-quick-btn.primary:hover { background:#1557d4; }
.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; }
.pb-panel-actions { padding:12px 20px; border-top:1px solid var(--pb-border); display:flex; gap:8px; background:var(--pb-surface); position:sticky; bottom:0; }
.pb-panel-actions .pb-quick-btn { flex:1; }
.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; text-align:center; }
</style>
<div class="pb-shell" x-data="{ view: 'kanban', dragKey: null }">
<div class="pb-shell" x-data="{ view: 'kanban', dragKey: null }" wire:poll.10s>
{{-- STAT STRIP --}}
<div class="pb-stat-strip">
<div class="pb-stat-item">
@@ -197,7 +219,7 @@
</div>
{{-- KANBAN --}}
<div class="pb-board" x-show="view==='kanban'" wire:poll.10s>
<div class="pb-board" x-show="view==='kanban'">
@foreach ($columns as $colKey => $col)
<div class="pb-col"
@dragover.prevent="$el.classList.add('over')"
@@ -229,9 +251,26 @@
@if ($card['urgent'])
<div class="pb-deal-urgent"></div>
@endif
<div class="pb-deal-actions" @click.stop>
@if (in_array($colKey, ['request', 'quote']))
<a class="pb-act-btn" wire:click.stop="quickSchedule('{{ $card['key'] }}')" title="Programare"
wire:loading.attr="disabled">📅</a>
@elseif ($colKey === 'scheduled')
<a class="pb-act-btn" wire:click.stop="moveCard('{{ $card['key'] }}', 'in_work')" title="Începe lucrul"></a>
@elseif ($colKey === 'in_work')
<a class="pb-act-btn" wire:click.stop="moveCard('{{ $card['key'] }}', 'ready')" title="Marchează Gata"></a>
@elseif ($colKey === 'ready')
<a class="pb-act-btn" wire:click.stop="moveCard('{{ $card['key'] }}', 'paid')" title="Achitat">💰</a>
@endif
@if (!empty($card['phone']))
<a class="pb-act-btn" href="tel:{{ $card['phone'] }}" @click.stop title="Sună">📞</a>
<a class="pb-act-btn" href="https://wa.me/{{ preg_replace('/\D/', '', $card['phone']) }}" target="_blank" @click.stop title="WhatsApp">💬</a>
@endif
<a class="pb-act-btn" href="{{ $card['edit_url'] }}" @click.stop title="Deschide"></a>
</div>
<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>
<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)
@@ -257,15 +296,20 @@
@endif
</div>
@empty
<div class="pb-empty-col">Gol</div>
<div class="pb-empty-col">Gol trage un card aici</div>
@endforelse
@if (in_array($colKey, ['request', 'quote', 'scheduled']))
<a class="pb-add-card" href="{{ route('filament.tenant.resources.leads.create') }}">+ Adaugă cerere</a>
@elseif (in_array($colKey, ['in_work', 'ready']))
<a class="pb-add-card" href="{{ route('filament.tenant.resources.work-orders.create') }}">+ Fișă nouă</a>
@endif
</div>
</div>
@endforeach
</div>
{{-- LIST VIEW --}}
<div x-show="view==='list'" style="padding:16px 20px;" x-cloak>
<div x-show="view==='list'" style="padding:16px 20px; flex:1; overflow:auto;" x-cloak>
<table style="width:100%; border-collapse:collapse; background:var(--pb-surface); border:1px solid var(--pb-border); border-radius:10px;">
<thead>
<tr>
@@ -305,7 +349,7 @@
<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>
<div class="pb-panel-sec-title">Progres etapă</div>
<div class="pb-stage-stepper">
@foreach ($detail['stages'] as $st)
@@ -349,7 +393,7 @@
@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>
<a class="pb-quick-btn" style="padding:4px 10px;" 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;">
@@ -382,16 +426,28 @@
@if (!empty($detail['phone']))
<div class="pb-panel-section">
<div class="pb-panel-sec-title">Acțiuni rapide</div>
<div class="pb-panel-sec-title">Contactare rapidă</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>
<a class="pb-quick-btn" href="{{ $detail['edit_url'] }}"> Editare</a>
</div>
</div>
@endif
</div>
{{-- Bottom action bar primary CTAs per stage --}}
<div class="pb-panel-actions">
@php $kind = explode(':', $openCardKey)[0] ?? '' @endphp
@if (in_array($kind, ['lead', 'deal']) && empty($detail['wo']))
<a class="pb-quick-btn" wire:click="quickSchedule('{{ $openCardKey }}')">📅 Programează</a>
<a class="pb-quick-btn primary" href="{{ $detail['edit_url'] }}">Editare deal</a>
@elseif ($kind === 'wo')
<a class="pb-quick-btn" href="{{ $this->calendarUrl() }}">📅 Calendar</a>
<a class="pb-quick-btn primary" href="{{ $detail['edit_url'] }}"> Deschide Fișă</a>
@endif
</div>
@endif
</div>
</div>