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
@@ -25,6 +25,21 @@ class PipelineBoard extends Page
protected string $view = 'filament.tenant.pages.pipeline-board';
public function getMaxContentWidth(): \Filament\Support\Enums\Width
{
return \Filament\Support\Enums\Width::Full;
}
public function getHeading(): string
{
return '';
}
public function getSubheading(): ?string
{
return null;
}
public string $activeFilter = 'all'; // all | mine | urgent | today
public ?string $openCardKey = null; // "lead:5" / "deal:8" / "wo:12"
@@ -269,6 +284,56 @@ class PipelineBoard extends Page
$this->openCardKey = $key;
}
/** Quick-schedule from a card: bumps the source to "Programat", creates an Appointment for tomorrow 10:00, returns calendar URL. */
public function quickSchedule(string $key): void
{
[$kind, $id] = explode(':', $key, 2) + [null, null];
$id = (int) $id;
if (! $kind || ! $id) return;
$clientId = null; $vehicleId = null; $dealId = null; $title = null; $masterId = null;
if ($kind === 'lead') {
$lead = Lead::find($id);
if (! $lead) return;
$deal = $lead->convert(['stage' => 'scheduled', 'scheduled_at' => now()->addDay()->setHour(10)->setMinute(0)]);
$clientId = $deal->client_id; $vehicleId = $deal->vehicle_id; $dealId = $deal->id;
$title = $deal->name;
} elseif ($kind === 'deal') {
$deal = Deal::find($id);
if (! $deal) return;
$deal->update(['stage' => 'scheduled', 'scheduled_at' => now()->addDay()->setHour(10)->setMinute(0)]);
$clientId = $deal->client_id; $vehicleId = $deal->vehicle_id; $dealId = $deal->id;
$title = $deal->name;
$masterId = $deal->assigned_to;
} elseif ($kind === 'wo') {
$wo = WorkOrder::find($id);
if (! $wo) return;
$clientId = $wo->client_id; $vehicleId = $wo->vehicle_id;
$title = $wo->number;
$masterId = $wo->master_id;
}
\App\Models\Tenant\Appointment::create([
'client_id' => $clientId,
'vehicle_id' => $vehicleId,
'master_id' => $masterId,
'deal_id' => $dealId,
'date' => today()->addDay(),
'time_start' => '10:00',
'time_end' => '11:00',
'title' => $title ?: 'Programare',
'status' => 'scheduled',
]);
$this->notify("Programare creată · mâine 10:00");
$this->openCardKey = null;
}
public function calendarUrl(): string
{
return route('filament.tenant.pages.calendar-board');
}
public function closeCard(): void
{
$this->openCardKey = null;