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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class VinDecoder
|
||||
{
|
||||
/** WMI (first 3 chars of VIN) → manufacturer/country (subset relevant for European cars). */
|
||||
public const WMI = [
|
||||
'WBA' => ['BMW', 'Germany'], 'WBS' => ['BMW M', 'Germany'],
|
||||
'WAU' => ['Audi', 'Germany'], 'WA1' => ['Audi SUV', 'Germany'],
|
||||
'WVW' => ['Volkswagen', 'Germany'], 'WV1' => ['VW Comm', 'Germany'], 'WV2' => ['VW Bus', 'Germany'],
|
||||
'WP0' => ['Porsche', 'Germany'], 'WP1' => ['Porsche SUV', 'Germany'],
|
||||
'WDB' => ['Mercedes-Benz', 'Germany'], 'WDC' => ['Mercedes SUV', 'Germany'], 'WDD' => ['Mercedes AMG', 'Germany'],
|
||||
'TMB' => ['Skoda', 'Czechia'],
|
||||
'JF1' => ['Subaru', 'Japan'], 'JS3' => ['Suzuki', 'Japan'],
|
||||
'JT1' => ['Toyota', 'Japan'], 'JTD' => ['Toyota Hybrid', 'Japan'],
|
||||
'JHM' => ['Honda', 'Japan'], '1HG' => ['Honda US', 'USA'],
|
||||
'KMH' => ['Hyundai', 'Korea'], 'KNA' => ['Kia', 'Korea'],
|
||||
'VF1' => ['Renault', 'France'], 'VF3' => ['Peugeot', 'France'], 'VF7' => ['Citroen', 'France'],
|
||||
'ZFA' => ['Fiat', 'Italy'], 'ZAR' => ['Alfa Romeo', 'Italy'],
|
||||
'WF0' => ['Ford EU', 'Germany'], '1FA' => ['Ford US', 'USA'],
|
||||
'YV1' => ['Volvo', 'Sweden'],
|
||||
'W0L' => ['Opel', 'Germany'],
|
||||
];
|
||||
|
||||
/** 10th VIN char (model year). */
|
||||
public const YEAR_CODES = [
|
||||
'A' => 2010, 'B' => 2011, 'C' => 2012, 'D' => 2013, 'E' => 2014, 'F' => 2015,
|
||||
'G' => 2016, 'H' => 2017, 'J' => 2018, 'K' => 2019, 'L' => 2020, 'M' => 2021,
|
||||
'N' => 2022, 'P' => 2023, 'R' => 2024, 'S' => 2025, 'T' => 2026,
|
||||
'1' => 2001, '2' => 2002, '3' => 2003, '4' => 2004, '5' => 2005,
|
||||
'6' => 2006, '7' => 2007, '8' => 2008, '9' => 2009,
|
||||
];
|
||||
|
||||
public function decode(string $vin): array
|
||||
{
|
||||
$vin = strtoupper(trim($vin));
|
||||
if (strlen($vin) < 11) {
|
||||
return ['valid' => false, 'error' => 'VIN trebuie să aibă cel puțin 11 caractere.'];
|
||||
}
|
||||
|
||||
$wmi = substr($vin, 0, 3);
|
||||
$manufacturer = self::WMI[$wmi] ?? null;
|
||||
$yearChar = $vin[9] ?? null;
|
||||
$year = self::YEAR_CODES[$yearChar] ?? null;
|
||||
|
||||
// Heuristic: 70s/80s/90s vs 2000s+ — model year alone is ambiguous,
|
||||
// we just return what we can.
|
||||
return [
|
||||
'valid' => $manufacturer !== null,
|
||||
'vin' => $vin,
|
||||
'wmi' => $wmi,
|
||||
'manufacturer' => $manufacturer[0] ?? '?',
|
||||
'country' => $manufacturer[1] ?? '?',
|
||||
'serial' => substr($vin, 11),
|
||||
'year' => $year,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<x-filament-panels::page>
|
||||
<style>
|
||||
.vs-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 20px; margin-bottom: 16px; }
|
||||
.dark .vs-card { background: #1f2937; border-color: #374151; }
|
||||
.vs-input {
|
||||
width: 100%; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 8px;
|
||||
font-family: monospace; font-size: 16px; letter-spacing: 1px; text-transform: uppercase;
|
||||
}
|
||||
.dark .vs-input { background: #111827; border-color: #374151; color: #f9fafb; }
|
||||
.vs-btn {
|
||||
padding: 10px 24px; background: #3b82f6; color: #fff; border: none;
|
||||
border-radius: 8px; font-weight: 500; cursor: pointer; margin-top: 12px;
|
||||
}
|
||||
.vs-btn:hover { background: #2563eb; }
|
||||
.vs-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
||||
.vs-stat-l { font-size: 11px; color: #6b7280; text-transform: uppercase; letter-spacing: .5px; }
|
||||
.vs-stat-v { font-size: 16px; font-weight: 600; margin-top: 4px; }
|
||||
.vs-tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.vs-tbl th, .vs-tbl td { padding: 8px; text-align: left; border-bottom: 1px solid #f3f4f6; }
|
||||
.vs-tbl th { background: #f9fafb; font-weight: 600; color: #6b7280; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
<div class="vs-card">
|
||||
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px;">🔎 Decode VIN</h2>
|
||||
<form wire:submit="decode" style="display:flex;gap:12px;flex-wrap:wrap;">
|
||||
<input type="text" wire:model="vin" class="vs-input" placeholder="WBAFE81070CY34521" maxlength="17" style="flex:1;min-width:300px;">
|
||||
<button type="submit" class="vs-btn">Decodează</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if ($decoded)
|
||||
<div class="vs-card">
|
||||
@if ($decoded['valid'])
|
||||
<h3 style="font-size:14px;font-weight:600;margin-bottom:16px;">✅ VIN decodificat</h3>
|
||||
<div class="vs-grid">
|
||||
<div><div class="vs-stat-l">Producător</div><div class="vs-stat-v">{{ $decoded['manufacturer'] }}</div></div>
|
||||
<div><div class="vs-stat-l">Țară</div><div class="vs-stat-v">{{ $decoded['country'] }}</div></div>
|
||||
<div><div class="vs-stat-l">An model (estimat)</div><div class="vs-stat-v">{{ $decoded['year'] ?? '—' }}</div></div>
|
||||
<div><div class="vs-stat-l">WMI</div><div class="vs-stat-v" style="font-family:monospace;">{{ $decoded['wmi'] }}</div></div>
|
||||
<div><div class="vs-stat-l">Serial</div><div class="vs-stat-v" style="font-family:monospace;font-size:12px;">{{ $decoded['serial'] }}</div></div>
|
||||
</div>
|
||||
@else
|
||||
<div style="color:#dc2626;">⚠ {{ $decoded['error'] ?? 'WMI necunoscut — producătorul nu e în baza de date locală.' }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php $matches = $this->vehicleMatches(); @endphp
|
||||
@if ($matches->isNotEmpty())
|
||||
<div class="vs-card">
|
||||
<h3 style="font-size:14px;font-weight:600;margin-bottom:12px;">🚗 Mașini cu acest VIN în CRM</h3>
|
||||
<table class="vs-tbl">
|
||||
<thead><tr><th>VIN</th><th>Marca/Model</th><th>Plate</th><th>Client</th><th>Km</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach ($matches as $v)
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:11px;">{{ $v->vin }}</td>
|
||||
<td>{{ $v->make }} {{ $v->model }} {{ $v->year }}</td>
|
||||
<td>{{ $v->plate ?? '—' }}</td>
|
||||
<td>{{ $v->client?->name ?? '—' }}</td>
|
||||
<td>{{ number_format($v->mileage, 0, '.', ' ') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="vs-card">
|
||||
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px;">📦 Caută piesă în catalog</h2>
|
||||
<input type="text" wire:model.live.debounce.300ms="partsQuery" class="vs-input"
|
||||
placeholder="Nume, cod articol, brand..." style="font-size:14px;text-transform:none;font-family:inherit;">
|
||||
|
||||
@php $parts = $this->partsResults(); @endphp
|
||||
@if ($partsQuery)
|
||||
<div style="margin-top:16px;">
|
||||
@if ($parts->isEmpty())
|
||||
<div style="color:#9ca3af;text-align:center;padding:20px;">Niciun rezultat pentru „{{ $partsQuery }}"</div>
|
||||
@else
|
||||
<table class="vs-tbl">
|
||||
<thead><tr><th>Denumire</th><th>Cod</th><th>Brand</th><th>Stoc</th><th>Preț</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach ($parts as $p)
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ route('filament.tenant.resources.parts.edit', ['record' => $p->id]) }}" style="color:inherit;">{{ $p->name }}</a>
|
||||
</td>
|
||||
<td style="font-family:monospace;font-size:11px;">{{ $p->article ?? '—' }}</td>
|
||||
<td>{{ $p->brand ?? '—' }}</td>
|
||||
<td style="color: {{ $p->qty <= 0 ? '#dc2626' : ($p->qty <= $p->min_qty ? '#d97706' : '#059669') }};">
|
||||
{{ $p->qty }} {{ $p->unit }}
|
||||
</td>
|
||||
<td>{{ number_format($p->sell_price, 2, '.', ' ') }} MDL</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,94 @@
|
||||
<x-filament-panels::page>
|
||||
@php $data = $this->data(); @endphp
|
||||
|
||||
<style>
|
||||
.wl-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.wl-btn {
|
||||
padding: 6px 12px; border: 1px solid #e5e7eb; border-radius: 6px;
|
||||
background: #fff; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.dark .wl-btn { background: #1f2937; border-color: #374151; color: #d1d5db; }
|
||||
.wl-week { font-weight: 600; font-size: 14px; }
|
||||
|
||||
.wl-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 16px; overflow-x: auto; }
|
||||
.dark .wl-card { background: #1f2937; border-color: #374151; }
|
||||
|
||||
.wl-tbl { width: 100%; border-collapse: separate; border-spacing: 4px; }
|
||||
.wl-tbl th, .wl-tbl td { padding: 8px; text-align: center; font-size: 12px; vertical-align: middle; }
|
||||
.wl-tbl th:first-child, .wl-tbl td:first-child { text-align: left; min-width: 120px; }
|
||||
.wl-cell {
|
||||
border-radius: 6px; padding: 12px 4px;
|
||||
font-weight: 600; min-height: 56px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
.wl-hours { font-size: 14px; }
|
||||
.wl-count { font-size: 10px; opacity: .85; margin-top: 2px; }
|
||||
|
||||
.wl-legend { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #6b7280; margin-top: 12px; }
|
||||
.wl-sw { display: inline-block; width: 16px; height: 12px; border-radius: 3px; }
|
||||
</style>
|
||||
|
||||
<div class="wl-bar">
|
||||
<button class="wl-btn" wire:click="setWeek(-1)">← Săpt. anterioară</button>
|
||||
<span class="wl-week">{{ $data['weekLabel'] }}</span>
|
||||
<button class="wl-btn" wire:click="setWeek(1)">Săpt. următoare →</button>
|
||||
<span style="margin-left:auto;font-size:11px;color:#6b7280;">capacitate ref: {{ $data['capacity'] }}h/zi</span>
|
||||
</div>
|
||||
|
||||
<div class="wl-card">
|
||||
<table class="wl-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@foreach ($data['days'] as $d)
|
||||
<th>{{ $d->translatedFormat('D') }}<br><span style="color:#9ca3af;font-weight:400;">{{ $d->format('d.m') }}</span></th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($data['matrix'] as $row)
|
||||
<tr>
|
||||
<td style="font-weight:600;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="width:12px;height:12px;border-radius:3px;background:{{ $row['post']->color ?? '#9ca3af' }};display:inline-block;"></span>
|
||||
{{ $row['post']->name }}
|
||||
</div>
|
||||
</td>
|
||||
@foreach ($row['days'] as $cell)
|
||||
@php
|
||||
$h = (float) $cell['hours'];
|
||||
$ratio = min(1, $h / max(0.001, $data['capacity']));
|
||||
// Heatmap color: green → yellow → red as ratio grows.
|
||||
if ($h <= 0) {
|
||||
$bg = '#f3f4f6'; $fg = '#9ca3af';
|
||||
} else {
|
||||
$r = (int) (255 * $ratio);
|
||||
$g = (int) (200 * (1 - $ratio));
|
||||
$bg = "rgba({$r},{$g},80,0.18)";
|
||||
$fg = "rgba({$r},{$g},80,1)";
|
||||
}
|
||||
@endphp
|
||||
<td>
|
||||
<div class="wl-cell" style="background:{{ $bg }};color:{{ $fg }};">
|
||||
@if ($h > 0)
|
||||
<span class="wl-hours">{{ number_format($h, 1) }}h</span>
|
||||
<span class="wl-count">{{ $cell['count'] }} progr.</span>
|
||||
@else
|
||||
<span style="color:#d1d5db;">—</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="wl-legend">
|
||||
Legendă:
|
||||
<span class="wl-sw" style="background:#f3f4f6;"></span> liber
|
||||
<span class="wl-sw" style="background:rgba(64,200,80,0.3);"></span> ușor
|
||||
<span class="wl-sw" style="background:rgba(160,160,80,0.3);"></span> mediu
|
||||
<span class="wl-sw" style="background:rgba(255,80,80,0.3);"></span> plin (peste capacitate)
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{ $name }} — autoservice {{ $city ?? '' }}</title>
|
||||
@if ($faviconUrl)
|
||||
<link rel="icon" href="{{ $faviconUrl }}">
|
||||
@endif
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: #1f2937; line-height: 1.5; }
|
||||
|
||||
.hero {
|
||||
background: linear-gradient(135deg, {{ $themeColor }}, {{ $themeColor }}cc);
|
||||
color: #fff; padding: 80px 20px; text-align: center;
|
||||
}
|
||||
.hero img { max-height: 80px; margin-bottom: 16px; }
|
||||
.hero h1 { font-size: 38px; font-weight: 700; margin-bottom: 12px; }
|
||||
.hero p { font-size: 18px; opacity: .92; max-width: 600px; margin: 0 auto; }
|
||||
|
||||
.section { padding: 60px 20px; max-width: 1100px; margin: 0 auto; }
|
||||
.section h2 { font-size: 28px; font-weight: 700; margin-bottom: 24px; text-align: center; }
|
||||
|
||||
.services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
|
||||
.service-card {
|
||||
background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 12px;
|
||||
padding: 24px; text-align: center; transition: transform .15s;
|
||||
}
|
||||
.service-card:hover { transform: translateY(-2px); border-color: {{ $themeColor }}; }
|
||||
.service-card .icon { font-size: 32px; margin-bottom: 12px; }
|
||||
.service-card h3 { font-size: 16px; font-weight: 600; }
|
||||
|
||||
.info { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; }
|
||||
.info-block { padding: 24px; background: #f9fafb; border-radius: 12px; }
|
||||
.info-block h3 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: {{ $themeColor }}; }
|
||||
.info-block p { font-size: 14px; color: #4b5563; margin-bottom: 6px; }
|
||||
|
||||
.cta { background: {{ $themeColor }}; color: #fff; text-align: center; padding: 60px 20px; }
|
||||
.cta h2 { font-size: 26px; margin-bottom: 16px; }
|
||||
.cta a {
|
||||
display: inline-block; padding: 14px 32px;
|
||||
background: #fff; color: {{ $themeColor }};
|
||||
border-radius: 8px; text-decoration: none; font-weight: 600;
|
||||
margin: 8px; transition: opacity .15s;
|
||||
}
|
||||
.cta a:hover { opacity: .9; }
|
||||
|
||||
footer { background: #1f2937; color: #9ca3af; padding: 24px; text-align: center; font-size: 13px; }
|
||||
footer a { color: #d1d5db; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="hero">
|
||||
@if ($logoUrl)
|
||||
<img src="{{ $logoUrl }}" alt="logo">
|
||||
@endif
|
||||
<h1>{{ $name }}</h1>
|
||||
<p>Autoservice profesional{{ $city ? ' — ' . $city : '' }}. Diagnostic, reparații, piese, ITP.</p>
|
||||
</header>
|
||||
|
||||
@if (! empty($services))
|
||||
<section class="section">
|
||||
<h2>Servicii oferite</h2>
|
||||
<div class="services-grid">
|
||||
@foreach ($services as $s)
|
||||
<div class="service-card">
|
||||
<div class="icon">🔧</div>
|
||||
<h3>{{ $s }}</h3>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<section class="section">
|
||||
<div class="info">
|
||||
<div class="info-block">
|
||||
<h3>📍 Locație & contact</h3>
|
||||
@if ($city) <p><b>Oraș:</b> {{ $city }}</p> @endif
|
||||
@if ($phone) <p><b>Telefon:</b> <a href="tel:{{ $phone }}" style="color:inherit;">{{ $phone }}</a></p> @endif
|
||||
@if ($email) <p><b>Email:</b> <a href="mailto:{{ $email }}" style="color:inherit;">{{ $email }}</a></p> @endif
|
||||
</div>
|
||||
<div class="info-block">
|
||||
<h3>🕒 Program de lucru</h3>
|
||||
<p>Luni – Vineri: 08:00 – 18:00</p>
|
||||
<p>Sâmbătă: 09:00 – 14:00</p>
|
||||
<p>Duminică: închis</p>
|
||||
</div>
|
||||
@if (! empty($cars))
|
||||
<div class="info-block">
|
||||
<h3>🚗 Mărci suportate</h3>
|
||||
<p>{{ implode(', ', array_slice($cars, 0, 12)) }}@if (count($cars) > 12), …@endif</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta">
|
||||
<h2>Rezervă o programare</h2>
|
||||
@if ($phone)
|
||||
<a href="tel:{{ $phone }}">📞 {{ $phone }}</a>
|
||||
@endif
|
||||
@if ($email)
|
||||
<a href="mailto:{{ $email }}">✉ {{ $email }}</a>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
© {{ date('Y') }} {{ $name }} · powered by AutoCRM
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+14
-3
@@ -5,9 +5,20 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
// On a tenant subdomain → redirect to the tenant panel.
|
||||
if (app(TenantManager::class)->isResolved()) {
|
||||
return redirect('/app');
|
||||
// On a tenant subdomain → public landing page.
|
||||
$tenant = app(TenantManager::class)->current();
|
||||
if ($tenant) {
|
||||
return view('site.landing', [
|
||||
'name' => $tenant->display_name ?? $tenant->name,
|
||||
'city' => $tenant->city,
|
||||
'phone' => $tenant->phone,
|
||||
'email' => $tenant->email,
|
||||
'themeColor' => $tenant->settings['theme_color'] ?? '#3B82F6',
|
||||
'services' => (array) ($tenant->settings['services'] ?? []),
|
||||
'cars' => (array) ($tenant->settings['cars'] ?? []),
|
||||
'logoUrl' => $tenant->getLogoUrl(),
|
||||
'faviconUrl' => $tenant->getFaviconUrl(),
|
||||
]);
|
||||
}
|
||||
// On the central domain → redirect to admin.
|
||||
return redirect('/admin');
|
||||
|
||||
Reference in New Issue
Block a user