Files
autocrm/app/Models/Tenant/Deal.php
T
Vasyka 3603c0e43b 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>
2026-06-04 20:02:44 +00:00

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);
}
}