Batch 2: Workload heatmap + Site PSauto + VIN search
═══ Workload heatmap (Încărcare STO) ═══ - /app/workload custom Page (group Analiză) - Săptămână (Lu-Du) × posturi → matrice ore programate - Heatmap colorat: verde→galben→roșu pe ratio capacity (10h/zi) - Navigare săpt anterior/curent/următor - Programări fără pod → row '— fără pod —' separat ═══ Site PSauto (landing public) ═══ - / pe tenant subdomain → resources/views/site/landing.blade.php - Hero cu logo + nume + slogan; gradient theme color - Servicii (din settings.services) — grid card-uri - Locație/contact + program lucru standardizat - Mărci suportate (din settings.cars) - CTA: phone + email - Footer cu tenant name + powered by AutoCRM ═══ VIN search ═══ - VinDecoder service: WMI hardcoded (24 producători EU/Asia/USA) + year codes (2001-2026) — pure offline, fără API extern - /app/vin-search Page (group Depozit) cu: • Input VIN cu uppercase + monospace • Decode → producător/țară/an/serial • Match VIN-uri din baza Vehicles • Search piese din catalog (live debounce 300ms) - Rezultatele linkează la editor Vehicle/Part Total tenant routes: 102.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Pages;
|
||||
|
||||
use App\Models\Tenant\Part;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use App\Services\VinDecoder;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class VinSearch extends Page
|
||||
{
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-magnifying-glass';
|
||||
|
||||
protected static ?string $navigationLabel = 'VIN-căutare';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Depozit';
|
||||
|
||||
protected static ?int $navigationSort = 41;
|
||||
|
||||
protected static ?string $title = 'VIN căutare & piese';
|
||||
|
||||
protected string $view = 'filament.tenant.pages.vin-search';
|
||||
|
||||
public string $vin = '';
|
||||
|
||||
public ?array $decoded = null;
|
||||
|
||||
public string $partsQuery = '';
|
||||
|
||||
public function decode(): void
|
||||
{
|
||||
if (! $this->vin) return;
|
||||
$this->decoded = app(VinDecoder::class)->decode($this->vin);
|
||||
}
|
||||
|
||||
public function vehicleMatches(): \Illuminate\Support\Collection
|
||||
{
|
||||
if (! $this->vin) return collect();
|
||||
return Vehicle::with('client')
|
||||
->where('vin', 'like', '%' . $this->vin . '%')
|
||||
->limit(10)->get();
|
||||
}
|
||||
|
||||
public function partsResults(): \Illuminate\Support\Collection
|
||||
{
|
||||
$q = trim($this->partsQuery);
|
||||
if ($q === '') return collect();
|
||||
return Part::where('is_active', true)
|
||||
->where(function ($query) use ($q) {
|
||||
$query->where('name', 'like', "%{$q}%")
|
||||
->orWhere('article', 'like', "%{$q}%")
|
||||
->orWhere('brand', 'like', "%{$q}%");
|
||||
})
|
||||
->limit(20)->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Tenant\Pages;
|
||||
|
||||
use App\Models\Tenant\Appointment;
|
||||
use App\Models\Tenant\Post;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Workload extends Page
|
||||
{
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-fire';
|
||||
|
||||
protected static ?string $navigationLabel = 'Încărcare STO';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Analiză';
|
||||
|
||||
protected static ?int $navigationSort = 73;
|
||||
|
||||
protected static ?string $title = 'Încărcare service';
|
||||
|
||||
protected string $view = 'filament.tenant.pages.workload';
|
||||
|
||||
public string $weekStart;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->weekStart = Carbon::now()->startOfWeek()->toDateString();
|
||||
}
|
||||
|
||||
public function setWeek(int $delta): void
|
||||
{
|
||||
$this->weekStart = Carbon::parse($this->weekStart)->addWeeks($delta)->toDateString();
|
||||
}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
$start = Carbon::parse($this->weekStart);
|
||||
$end = (clone $start)->addDays(6);
|
||||
|
||||
$posts = Post::where('is_active', true)->orderBy('sort_order')->get();
|
||||
$days = collect(range(0, 6))->map(fn ($i) => $start->copy()->addDays($i));
|
||||
|
||||
// Appointments grouped by post + day.
|
||||
$appts = Appointment::whereBetween('date', [$start, $end])
|
||||
->whereNotIn('status', ['cancelled', 'no_show'])
|
||||
->get()
|
||||
->groupBy(fn ($a) => $a->post_id . '|' . $a->date->format('Y-m-d'));
|
||||
|
||||
// Compute total hours per cell.
|
||||
$matrix = [];
|
||||
foreach ($posts as $post) {
|
||||
$matrix[$post->id] = ['post' => $post, 'days' => []];
|
||||
foreach ($days as $d) {
|
||||
$key = $post->id . '|' . $d->format('Y-m-d');
|
||||
$items = $appts->get($key, collect());
|
||||
$hours = $items->sum(function ($a) {
|
||||
$start = strtotime("1970-01-01 {$a->time_start}");
|
||||
$end = strtotime("1970-01-01 {$a->time_end}");
|
||||
return max(0, ($end - $start) / 3600);
|
||||
});
|
||||
$matrix[$post->id]['days'][$d->format('Y-m-d')] = [
|
||||
'date' => $d,
|
||||
'hours' => $hours,
|
||||
'count' => $items->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Same for appointments without post (drop them on a virtual "—" row).
|
||||
$unposted = $appts->keys()->filter(fn ($k) => str_starts_with($k, '|'));
|
||||
if ($unposted->isNotEmpty()) {
|
||||
$matrix[0] = ['post' => (object) ['id' => 0, 'name' => '— fără pod —', 'color' => '#9ca3af'], 'days' => []];
|
||||
foreach ($days as $d) {
|
||||
$key = '|' . $d->format('Y-m-d');
|
||||
$items = $appts->get($key, collect());
|
||||
$hours = $items->sum(function ($a) {
|
||||
$start = strtotime("1970-01-01 {$a->time_start}");
|
||||
$end = strtotime("1970-01-01 {$a->time_end}");
|
||||
return max(0, ($end - $start) / 3600);
|
||||
});
|
||||
$matrix[0]['days'][$d->format('Y-m-d')] = ['date' => $d, 'hours' => $hours, 'count' => $items->count()];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'matrix' => $matrix,
|
||||
'days' => $days,
|
||||
'weekLabel' => $start->format('d.m') . ' – ' . $end->format('d.m.Y'),
|
||||
// Working day = 10h. Color scale: 0..10h → light to red.
|
||||
'capacity' => 10,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user