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:
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('deals', function (Blueprint $t) {
|
||||
if (! Schema::hasColumn('deals', 'urgent')) {
|
||||
$t->boolean('urgent')->default(false)->after('source');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'quote_sent_at')) {
|
||||
$t->timestamp('quote_sent_at')->nullable()->after('urgent');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'quote_status')) {
|
||||
$t->string('quote_status', 16)->nullable()->after('quote_sent_at');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'quote_seen_at')) {
|
||||
$t->timestamp('quote_seen_at')->nullable()->after('quote_status');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'scheduled_at')) {
|
||||
$t->timestamp('scheduled_at')->nullable()->after('quote_seen_at');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'bay')) {
|
||||
$t->string('bay', 32)->nullable()->after('scheduled_at');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'confirmed_at')) {
|
||||
$t->timestamp('confirmed_at')->nullable()->after('bay');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'confirmed_via')) {
|
||||
$t->string('confirmed_via', 16)->nullable()->after('confirmed_at');
|
||||
}
|
||||
if (! Schema::hasColumn('deals', 'last_action_at')) {
|
||||
$t->timestamp('last_action_at')->nullable()->after('confirmed_via');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('deals', function (Blueprint $t) {
|
||||
foreach (['urgent', 'quote_sent_at', 'quote_status', 'quote_seen_at', 'scheduled_at', 'bay', 'confirmed_at', 'confirmed_via', 'last_action_at'] as $col) {
|
||||
if (Schema::hasColumn('deals', $col)) {
|
||||
$t->dropColumn($col);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user