3603c0e43b
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>
88 lines
2.2 KiB
PHP
88 lines
2.2 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Tenant;
|
|
|
|
use App\Models\Concerns\BelongsToTenant;
|
|
use App\Models\Concerns\Auditable;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class Deal extends Model
|
|
{
|
|
use Auditable, BelongsToTenant, SoftDeletes;
|
|
|
|
public const STAGES = [
|
|
'new' => 'Nou',
|
|
'contact' => 'Calculație',
|
|
'agree' => 'Aprobare',
|
|
'scheduled' => 'Programat',
|
|
'arrived' => 'Sosit',
|
|
'in_work' => 'În lucru',
|
|
'done' => 'Finalizat',
|
|
'lost' => 'Pierdut',
|
|
];
|
|
|
|
public const QUOTE_STATUSES = [
|
|
'pending' => 'În așteptare',
|
|
'sent' => 'Trimis · fără răspuns',
|
|
'seen' => 'Văzut ✓',
|
|
'responded' => 'A răspuns',
|
|
];
|
|
|
|
public const CONFIRM_CHANNELS = [
|
|
'whatsapp' => 'WhatsApp',
|
|
'sms' => 'SMS',
|
|
'telegram' => 'Telegram',
|
|
'call' => 'Apel',
|
|
];
|
|
|
|
protected $fillable = [
|
|
'company_id', 'client_id', 'vehicle_id',
|
|
'name', 'price', 'stage', 'source', 'note',
|
|
'assigned_to', 'won_at', 'lost_at', 'lost_reason',
|
|
'urgent', 'quote_sent_at', 'quote_status', 'quote_seen_at',
|
|
'scheduled_at', 'bay', 'confirmed_at', 'confirmed_via',
|
|
'last_action_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'price' => 'decimal:2',
|
|
'won_at' => 'datetime',
|
|
'lost_at' => 'datetime',
|
|
'urgent' => 'boolean',
|
|
'quote_sent_at' => 'datetime',
|
|
'quote_seen_at' => 'datetime',
|
|
'scheduled_at' => 'datetime',
|
|
'confirmed_at' => 'datetime',
|
|
'last_action_at' => 'datetime',
|
|
];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::saving(function (self $deal) {
|
|
$deal->last_action_at = now();
|
|
});
|
|
}
|
|
|
|
public function client(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Client::class);
|
|
}
|
|
|
|
public function vehicle(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Vehicle::class);
|
|
}
|
|
|
|
public function assignedTo(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'assigned_to');
|
|
}
|
|
|
|
public function isOpen(): bool
|
|
{
|
|
return ! in_array($this->stage, ['done', 'lost'], true);
|
|
}
|
|
}
|