feat: Pipeline board matches mockup pixel-by-pixel
Audit pass against /tmp/service/todo/psauto-pipeline-redesign.html — 10
gaps closed.
1. In-page TOPBAR (mockup had it; was missing): "Pipeline" title,
sep, search box "Caută client, mașină, număr...", and right-side
Filtre / Export / + Deal nou (primary) buttons. Search input is
wire:model.live.debounce 300ms.
2. SEARCH actually filters cards: $searchQuery property in
PipelineBoard scans subject + client_name + plate + code + phone
across all 6 columns, case-insensitive.
3. "+ Deal nou" + "+ Adaugă cerere" (per-column bottom) now open the
SAME right-side panel in "new form" mode. Inline create form:
Nume / Telefon / Auto / Sursă / Notițe → createNewLead() inserts
Lead with status=new, lands in col 1 instantly without leaving page.
Validation: name + phone required.
4. EXPORT button calls exportCsv() — streams a CSV of current filtered
columns (etapă, cod, subiect, client, telefon, auto, sumă,
responsabil, stare timp).
5. PERIOD selector chip shows current month in Romanian
(now()->locale('ro')->isoFormat('MMMM YYYY')) — matches "Iunie 2026".
6. HOVER icons now match mockup exactly per column:
- request: 📅 schedule / 📞 phone / ⋮ edit
- quote: 📅 schedule / 💬 wa / ⋮ edit
- scheduled: 📄 file-plus (start WO) / 💬 wa / ⋮ edit
- in_work: 👁 eye (open WO) / 💬 wa / ✓ mark Gata
- ready: 💰 cash (mark paid) / 📞 phone / ⋮ edit
- paid: NONE (col 6 has no hover actions per mockup)
7. Col 6 "Achitat azi" cards now opacity:0.65, no hover actions,
no time line, no assignee name (just avatar) — exactly as in mockup.
8. Sum display: amount == 0 renders "—" instead of "0 MDL", both in
card footer and list view.
9. "Avans achitat" tag (blue) appears on Ready cards with partial
payment (pay_status='partial'); "Neachitat" amber only when fully
unpaid. Matches mockup col 5 example "Nissan Qashqai · Gata +
Avans achitat".
10. Link tracking quick-action: appears in detail panel "Acțiuni rapide"
grid when WO has tracking_url. Sits alongside WhatsApp / Sună / SMS.
Two-panel architecture: $showNewForm and $openCardKey are mutually
exclusive. Click outside or ✕ closes the panel; opening one closes
the other.
Tests: +4 (createNewLead happy path, validation, search filter,
partial payment tag). Suite 185/185 (was 181).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,13 @@ class PipelineBoard extends Page
|
||||
|
||||
public string $activeFilter = 'all'; // all | mine | urgent | today
|
||||
public ?string $openCardKey = null; // "lead:5" / "deal:8" / "wo:12"
|
||||
public bool $showNewForm = false; // panel in "new request" mode
|
||||
public string $searchQuery = '';
|
||||
public string $newName = '';
|
||||
public string $newPhone = '';
|
||||
public string $newCar = '';
|
||||
public string $newSource = 'call';
|
||||
public string $newNotes = '';
|
||||
|
||||
public const COLUMNS = [
|
||||
'request' => ['Cerere nouă', '#94A3B8'],
|
||||
@@ -124,6 +131,18 @@ class PipelineBoard extends Page
|
||||
$cards['paid'][] = $this->woCard($wo);
|
||||
}
|
||||
|
||||
// Apply search query
|
||||
$q = trim($this->searchQuery);
|
||||
if ($q !== '') {
|
||||
$needle = mb_strtolower($q);
|
||||
foreach ($cards as $col => $list) {
|
||||
$cards[$col] = array_values(array_filter($list, function ($c) use ($needle) {
|
||||
$hay = mb_strtolower(($c['subject'] ?? '') . ' ' . ($c['client_name'] ?? '') . ' ' . ($c['plate'] ?? '') . ' ' . ($c['code'] ?? '') . ' ' . ($c['phone'] ?? ''));
|
||||
return str_contains($hay, $needle);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: urgent first, then time
|
||||
foreach ($cards as $col => $list) {
|
||||
usort($list, fn ($a, $b) => ($b['urgent'] ?? false) <=> ($a['urgent'] ?? false));
|
||||
@@ -188,6 +207,52 @@ class PipelineBoard extends Page
|
||||
$this->activeFilter = in_array($filter, ['all', 'mine', 'urgent', 'today'], true) ? $filter : 'all';
|
||||
}
|
||||
|
||||
public function openNewForm(): void
|
||||
{
|
||||
$this->showNewForm = true;
|
||||
$this->openCardKey = null;
|
||||
$this->newName = '';
|
||||
$this->newPhone = '';
|
||||
$this->newCar = '';
|
||||
$this->newSource = 'call';
|
||||
$this->newNotes = '';
|
||||
}
|
||||
|
||||
public function createNewLead(): void
|
||||
{
|
||||
$data = ['name' => trim($this->newName), 'phone' => trim($this->newPhone), 'car' => trim($this->newCar) ?: null, 'source' => $this->newSource, 'message' => trim($this->newNotes) ?: null];
|
||||
if ($data['name'] === '' || $data['phone'] === '') {
|
||||
$this->notify('Nume și telefon sunt obligatorii');
|
||||
return;
|
||||
}
|
||||
Lead::create(array_merge($data, ['status' => 'new']));
|
||||
$this->showNewForm = false;
|
||||
$this->notify('Cerere nouă adăugată');
|
||||
}
|
||||
|
||||
public function exportCsv()
|
||||
{
|
||||
$columns = $this->getColumns();
|
||||
$csv = "Etapă,Cod,Subiect,Client,Telefon,Auto,Sumă,Responsabil,Stare\n";
|
||||
foreach ($columns as $col) {
|
||||
foreach ($col['cards'] as $card) {
|
||||
$csv .= sprintf(
|
||||
"%s,%s,%s,%s,%s,%s,%.2f,%s,%s\n",
|
||||
$col['label'],
|
||||
$card['code'],
|
||||
str_replace(',', ' ', $card['subject']),
|
||||
str_replace(',', ' ', $card['client_name']),
|
||||
$card['phone'] ?? '',
|
||||
$card['plate'],
|
||||
$card['amount'],
|
||||
$card['assignee']['name'],
|
||||
str_replace(',', ' ', $card['time_text']),
|
||||
);
|
||||
}
|
||||
}
|
||||
return response()->streamDownload(fn () => print $csv, 'pipeline-' . today()->format('Y-m-d') . '.csv', ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
public function moveCard(string $key, string $toCol): void
|
||||
{
|
||||
[$kind, $id] = explode(':', $key, 2) + [null, null];
|
||||
@@ -475,7 +540,9 @@ class PipelineBoard extends Page
|
||||
}
|
||||
if ($wo->status === 'ready') {
|
||||
$tags[] = ['label' => 'Gata', 'color' => 'green'];
|
||||
if ($wo->pay_status !== 'paid') {
|
||||
if ($wo->pay_status === 'partial') {
|
||||
$tags[] = ['label' => 'Avans achitat', 'color' => 'blue'];
|
||||
} elseif ($wo->pay_status !== 'paid') {
|
||||
$tags[] = ['label' => 'Neachitat', 'color' => 'amber'];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user