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>
120 lines
4.9 KiB
PHP
120 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Tenant\Resources;
|
|
|
|
use App\Filament\Tenant\Resources\DealResource\Pages;
|
|
use App\Models\Tenant\Client;
|
|
use App\Models\Tenant\Deal;
|
|
use App\Models\Tenant\Lead;
|
|
use App\Models\Tenant\User;
|
|
use App\Models\Tenant\Vehicle;
|
|
use Filament\Forms;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Actions;
|
|
use Filament\Schemas;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
|
|
class DealResource extends Resource
|
|
{
|
|
protected static ?string $model = Deal::class;
|
|
|
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-funnel';
|
|
|
|
protected static ?string $navigationLabel = 'Pipeline (tabel)';
|
|
|
|
protected static string|\UnitEnum|null $navigationGroup = 'CRM';
|
|
|
|
public static function shouldRegisterNavigation(): bool
|
|
{
|
|
return false; // PipelineBoard page is the canonical entry; this resource keeps CRUD routes for edit/create.
|
|
}
|
|
|
|
protected static ?string $modelLabel = 'deal';
|
|
|
|
protected static ?string $pluralModelLabel = 'deal-uri';
|
|
|
|
protected static ?int $navigationSort = 6;
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema->components([
|
|
Schemas\Components\Section::make('Detalii')
|
|
->columns(2)
|
|
->schema([
|
|
Forms\Components\Select::make('client_id')
|
|
->label('Client')
|
|
->options(fn () => Client::pluck('name', 'id'))
|
|
->searchable()
|
|
->required(),
|
|
Forms\Components\Select::make('vehicle_id')
|
|
->label('Auto')
|
|
->options(fn (Schemas\Components\Utilities\Get $get) => $get('client_id')
|
|
? Vehicle::where('client_id', $get('client_id'))->pluck('plate', 'id')
|
|
: [])
|
|
->searchable(),
|
|
Forms\Components\TextInput::make('name')->label('Subiect')->required()->maxLength(160),
|
|
Forms\Components\TextInput::make('price')->label('Valoare')->numeric()->default(0),
|
|
Forms\Components\Select::make('stage')
|
|
->options(Deal::STAGES)
|
|
->default('new')
|
|
->required(),
|
|
Forms\Components\Select::make('source')
|
|
->options(Lead::SOURCES)
|
|
->searchable(),
|
|
Forms\Components\Select::make('assigned_to')
|
|
->label('Responsabil')
|
|
->options(fn () => User::pluck('name', 'id'))
|
|
->searchable(),
|
|
]),
|
|
Forms\Components\Textarea::make('note')->label('Notițe')->columnSpanFull()->rows(3),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('id')->label('#')->sortable(),
|
|
Tables\Columns\TextColumn::make('name')->label('Subiect')->searchable()->limit(40),
|
|
Tables\Columns\TextColumn::make('client.name')->label('Client')->searchable(),
|
|
Tables\Columns\TextColumn::make('vehicle.plate')->label('Auto')->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('stage')
|
|
->formatStateUsing(fn ($state) => Deal::STAGES[$state] ?? $state)
|
|
->badge()
|
|
->colors([
|
|
'gray' => ['new'],
|
|
'info' => ['contact', 'agree'],
|
|
'warning' => ['scheduled', 'arrived', 'in_work'],
|
|
'success' => ['done'],
|
|
'danger' => ['lost'],
|
|
]),
|
|
Tables\Columns\TextColumn::make('price')->money('MDL')->sortable(),
|
|
Tables\Columns\TextColumn::make('source')->label('Sursă')->formatStateUsing(fn ($state) => Lead::SOURCES[$state] ?? $state)->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('assignedTo.name')->label('Responsabil')->placeholder('—'),
|
|
Tables\Columns\TextColumn::make('created_at')->date()->sortable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\SelectFilter::make('stage')->options(Deal::STAGES),
|
|
Tables\Filters\SelectFilter::make('assigned_to')
|
|
->label('Responsabil')
|
|
->options(fn () => User::pluck('name', 'id')),
|
|
])
|
|
->actions([
|
|
Actions\EditAction::make(),
|
|
Actions\DeleteAction::make(),
|
|
])
|
|
->defaultSort('created_at', 'desc');
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListDeals::route('/'),
|
|
'create' => Pages\CreateDeal::route('/create'),
|
|
'edit' => Pages\EditDeal::route('/{record}/edit'),
|
|
];
|
|
}
|
|
}
|