feat: Pipeline board full-bleed + hover actions + Programare→Calendar

Addresses the gaps surfaced after the first redesign — the board was still
boxed in Filament chrome (not truly full-page), hover floating-actions and
"+ Adaugă" CTAs were missing, and the P0 "Programează" from deal card had
no calendar wiring.

Full-page:
- getMaxContentWidth() = Width::Full
- getHeading()/getSubheading() return empty so Filament's title bar
  disappears, leaving the kanban edge-to-edge
- CSS uses :has(.pb-shell) to strip Filament's page padding + heading
  block at the layout level
- Board height = calc(100vh - 64px); columns scroll independently

Hover floating-actions on every card (column-aware):
- Cols 1-2 (Cerere / Calculație): 📅 quickSchedule
- Col 3 (Programat): ▶ start work (creates WO)
- Col 4 (În lucru): ✓ mark Gata
- Col 5 (Gata): 💰 mark Achitat
- All cards with phone: 📞 tel: + 💬 wa.me
- All cards: ↗ open in resource edit
- Shown only on .pb-deal:hover, positioned absolute top-right

"+ Adaugă" CTA at column bottom:
- Cols 1-3 → /app/leads/create
- Cols 4-5 → /app/work-orders/create

Programare → Calendar (P0 AAA):
- quickSchedule($key) on PipelineBoard creates a real Appointment row for
  tomorrow 10:00 linked to (client_id, vehicle_id, master_id, deal_id),
  sets deal.stage='scheduled' + scheduled_at, then shows a toast
- Panel bottom action bar gains "📅 Programează" CTA for lead/deal cards
- "📅 Calendar" jump CTA for WO cards
- calendarUrl() returns the canonical filament.tenant.pages.calendar-board
  route

Empty column state now reads "Gol — trage un card aici" instead of just
"Gol" so the drop affordance is explicit.

Stat strip + filter bar sticky at top; board fills the remaining viewport.

Tests: +1 (quickSchedule creates Appointment + moves deal). Suite 181/181.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:14:20 +00:00
parent 3603c0e43b
commit 3c0f3ba39e
3 changed files with 160 additions and 21 deletions
+18
View File
@@ -153,6 +153,24 @@ class PipelineBoardTest extends TestCase
$this->assertEquals('mine', $cols['request']['cards'][0]['subject']);
}
public function test_quick_schedule_creates_appointment_and_moves_deal_to_scheduled(): void
{
$client = Client::create(['name' => 'C', 'phone' => '+37399123000', 'type' => 'individual', 'status' => 'active']);
$vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'SCH-1']);
$deal = Deal::create(['client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'name' => 'X Y — Repair', 'price' => 1000, 'stage' => 'contact']);
Livewire::test(PipelineBoard::class)->call('quickSchedule', "deal:{$deal->id}");
$deal->refresh();
$this->assertEquals('scheduled', $deal->stage);
$this->assertNotNull($deal->scheduled_at);
$apt = \App\Models\Tenant\Appointment::where('client_id', $client->id)->first();
$this->assertNotNull($apt);
$this->assertEquals('scheduled', $apt->status);
$this->assertEquals(today()->addDay()->toDateString(), $apt->date->toDateString());
}
public function test_open_card_loads_panel_detail_for_deal(): void
{
$client = Client::create(['name' => 'Ion Popescu', 'phone' => '+37369123456', 'type' => 'individual', 'status' => 'active']);