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:
@@ -153,6 +153,66 @@ class PipelineBoardTest extends TestCase
|
||||
$this->assertEquals('mine', $cols['request']['cards'][0]['subject']);
|
||||
}
|
||||
|
||||
public function test_create_new_lead_inserts_lead_in_request_column(): void
|
||||
{
|
||||
Livewire::test(PipelineBoard::class)
|
||||
->set('newName', 'Anonim X')
|
||||
->set('newPhone', '+37388999777')
|
||||
->set('newCar', 'Audi A4')
|
||||
->set('newSource', 'site')
|
||||
->set('newNotes', 'are zgomot la pornire')
|
||||
->call('createNewLead')
|
||||
->assertSet('showNewForm', false);
|
||||
|
||||
$this->assertDatabaseHas('leads', [
|
||||
'name' => 'Anonim X',
|
||||
'phone' => '+37388999777',
|
||||
'car' => 'Audi A4',
|
||||
'source' => 'site',
|
||||
'status' => 'new',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_new_lead_requires_name_and_phone(): void
|
||||
{
|
||||
Livewire::test(PipelineBoard::class)
|
||||
->set('newName', '')
|
||||
->set('newPhone', '+37300000000')
|
||||
->call('createNewLead');
|
||||
|
||||
$this->assertEquals(0, Lead::count());
|
||||
}
|
||||
|
||||
public function test_search_query_filters_cards_across_columns(): void
|
||||
{
|
||||
$client1 = Client::create(['name' => 'Muntean Alex', 'phone' => '+37399000001', 'type' => 'individual', 'status' => 'active']);
|
||||
$client2 = Client::create(['name' => 'Cojocaru Ion', 'phone' => '+37399000002', 'type' => 'individual', 'status' => 'active']);
|
||||
Deal::create(['client_id' => $client1->id, 'name' => 'VW Passat — Frâne', 'price' => 1000, 'stage' => 'new']);
|
||||
Deal::create(['client_id' => $client2->id, 'name' => 'Renault Megane — Diag', 'price' => 1500, 'stage' => 'new']);
|
||||
|
||||
$page = new PipelineBoard;
|
||||
$page->searchQuery = 'Muntean';
|
||||
$cols = $page->getColumns();
|
||||
|
||||
$this->assertEquals(1, $cols['request']['count']);
|
||||
$this->assertEquals('VW Passat — Frâne', $cols['request']['cards'][0]['subject']);
|
||||
}
|
||||
|
||||
public function test_partial_payment_shows_avans_achitat_tag_on_ready_card(): void
|
||||
{
|
||||
$client = Client::create(['name' => 'C', 'phone' => '+37399112233', 'type' => 'individual', 'status' => 'active']);
|
||||
$wo = WorkOrder::create(['number' => WorkOrder::generateNumber($this->company->id), 'client_id' => $client->id, 'opened_at' => today(), 'status' => 'ready', 'total' => 7100]);
|
||||
// Half payment → partial
|
||||
Payment::create(['client_id' => $client->id, 'work_order_id' => $wo->id, 'paid_at' => today(), 'amount' => 3000, 'method' => 'cash']);
|
||||
|
||||
$cols = (new PipelineBoard)->getColumns();
|
||||
$cards = $cols['ready']['cards'];
|
||||
$this->assertCount(1, $cards);
|
||||
$tagLabels = array_column($cards[0]['tags'], 'label');
|
||||
$this->assertContains('Gata', $tagLabels);
|
||||
$this->assertContains('Avans achitat', $tagLabels);
|
||||
}
|
||||
|
||||
public function test_quick_schedule_creates_appointment_and_moves_deal_to_scheduled(): void
|
||||
{
|
||||
$client = Client::create(['name' => 'C', 'phone' => '+37399123000', 'type' => 'individual', 'status' => 'active']);
|
||||
|
||||
Reference in New Issue
Block a user