diff --git a/app/Filament/Tenant/Pages/Onboarding.php b/app/Filament/Tenant/Pages/Onboarding.php new file mode 100644 index 0000000..a9e151f --- /dev/null +++ b/app/Filament/Tenant/Pages/Onboarding.php @@ -0,0 +1,146 @@ +current(); + if (! $company) abort(404); + + // Already onboarded → redirect to dashboard + if (! empty($company->settings['onboarded_at'])) { + redirect('/app'); + return; + } + + $s = (array) ($company->settings ?? []); + $this->form->fill([ + 'display_name' => $company->display_name ?? $company->name, + 'city' => $company->city, + 'phone' => $company->phone, + 'currency' => $s['currency'] ?? 'MDL', + 'language' => $s['language'] ?? 'ro', + 'theme_color' => $s['theme_color'] ?? '#3B82F6', + 'labor_rate' => $s['labor_rate'] ?? 400, + ]); + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + Schemas\Components\Section::make('Pas 1 — Datele afacerii') + ->visible(fn () => $this->step === 1) + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('display_name') + ->label('Denumire afișată')->required()->maxLength(120), + Forms\Components\TextInput::make('city') + ->label('Oraș')->maxLength(60), + Forms\Components\TextInput::make('phone') + ->label('Telefon principal')->tel()->maxLength(40), + Forms\Components\TextInput::make('currency') + ->label('Monedă')->required()->maxLength(8), + ]), + Schemas\Components\Section::make('Pas 2 — Brand & limbă') + ->visible(fn () => $this->step === 2) + ->columns(2) + ->schema([ + Forms\Components\Select::make('language') + ->label('Limbă') + ->options(['ro' => 'Română', 'ru' => 'Русский', 'en' => 'English']) + ->required(), + Forms\Components\ColorPicker::make('theme_color') + ->label('Culoare brand'), + Forms\Components\FileUpload::make('logo') + ->label('Logo (opțional)') + ->image()->imageEditor()->disk('public') + ->directory('tmp-uploads')->visibility('public') + ->maxSize(2048), + ]), + Schemas\Components\Section::make('Pas 3 — Tarif & terminat') + ->visible(fn () => $this->step === 3) + ->columns(1) + ->schema([ + Forms\Components\TextInput::make('labor_rate') + ->label('Tarif normo-oră (poți schimba oricând)') + ->numeric()->required(), + ]), + ]) + ->statePath('data'); + } + + public function next(): void + { + $this->form->getState(); + $this->step = min(3, $this->step + 1); + } + + public function prev(): void + { + $this->step = max(1, $this->step - 1); + } + + public function finish(): void + { + $data = $this->form->getState(); + $company = app(TenantManager::class)->current(); + if (! $company) return; + + $company->update([ + 'display_name' => $data['display_name'] ?? $company->display_name, + 'city' => $data['city'] ?? $company->city, + 'phone' => $data['phone'] ?? $company->phone, + 'settings' => array_merge((array) $company->settings, [ + 'currency' => $data['currency'] ?? 'MDL', + 'language' => $data['language'] ?? 'ro', + 'theme_color' => $data['theme_color'] ?? '#3B82F6', + 'labor_rate' => (float) ($data['labor_rate'] ?? 400), + 'onboarded_at' => now()->toIso8601String(), + ]), + ]); + + // Logo upload + if (! empty($data['logo'])) { + $abs = \Illuminate\Support\Facades\Storage::disk('public')->path($data['logo']); + if (file_exists($abs)) { + $company->clearMediaCollection('logo'); + $company->addMedia($abs)->preservingOriginal()->toMediaCollection('logo'); + @unlink($abs); + } + } + + Notification::make() + ->title('🎉 Bun venit în AutoCRM!') + ->body('Setările au fost salvate. Hai să adăugăm primul client.') + ->success() + ->send(); + + redirect('/app/clients/create'); + } +} diff --git a/app/Filament/Tenant/Resources/ClientResource.php b/app/Filament/Tenant/Resources/ClientResource.php index 04c4266..2477422 100644 --- a/app/Filament/Tenant/Resources/ClientResource.php +++ b/app/Filament/Tenant/Resources/ClientResource.php @@ -122,6 +122,9 @@ class ClientResource extends Resource Actions\EditAction::make(), Actions\DeleteAction::make(), ]) + ->emptyStateHeading('Niciun client încă') + ->emptyStateDescription('Adaugă primul tău client manual sau importă din CSV. Toate mașinile, fișele și plățile se vor lega automat de el.') + ->emptyStateIcon('heroicon-o-users') ->defaultSort('created_at', 'desc'); } diff --git a/app/Filament/Tenant/Resources/LeadResource.php b/app/Filament/Tenant/Resources/LeadResource.php index 92f08c8..98f51d9 100644 --- a/app/Filament/Tenant/Resources/LeadResource.php +++ b/app/Filament/Tenant/Resources/LeadResource.php @@ -134,6 +134,9 @@ class LeadResource extends Resource Actions\EditAction::make(), Actions\DeleteAction::make(), ]) + ->emptyStateHeading('Nicio cerere primită') + ->emptyStateDescription('Aici apar cererile clienților potențiali. Convertește-le în deal-uri sau direct în programări de la butonul „Convertește".') + ->emptyStateIcon('heroicon-o-inbox-arrow-down') ->defaultSort('created_at', 'desc'); } diff --git a/app/Filament/Tenant/Resources/PartResource.php b/app/Filament/Tenant/Resources/PartResource.php index 9d821ec..d69a392 100644 --- a/app/Filament/Tenant/Resources/PartResource.php +++ b/app/Filament/Tenant/Resources/PartResource.php @@ -131,6 +131,9 @@ class PartResource extends Resource Actions\EditAction::make(), Actions\DeleteAction::make(), ]) + ->emptyStateHeading('Depozit gol') + ->emptyStateDescription('Adaugă piese manual, sau folosește Achiziții ca să le adaugi prin recepție de la furnizor (cu prețuri și stoc auto). Procentaj poate seta automat prețul de vânzare.') + ->emptyStateIcon('heroicon-o-cube') ->defaultSort('name'); } diff --git a/app/Filament/Tenant/Resources/VehicleResource.php b/app/Filament/Tenant/Resources/VehicleResource.php index 1723815..51b98d9 100644 --- a/app/Filament/Tenant/Resources/VehicleResource.php +++ b/app/Filament/Tenant/Resources/VehicleResource.php @@ -96,6 +96,9 @@ class VehicleResource extends Resource Actions\EditAction::make(), Actions\DeleteAction::make(), ]) + ->emptyStateHeading('Nicio mașină încă') + ->emptyStateDescription('Adaugă mașini manual sau importă din CSV. Folosește VIN-căutare pentru decoder rapid și completare automată brand/model/an.') + ->emptyStateIcon('heroicon-o-truck') ->defaultSort('created_at', 'desc'); } diff --git a/app/Filament/Tenant/Resources/WorkOrderResource.php b/app/Filament/Tenant/Resources/WorkOrderResource.php index d60d241..954b8d4 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource.php @@ -169,6 +169,9 @@ class WorkOrderResource extends Resource Actions\EditAction::make(), Actions\DeleteAction::make(), ]) + ->emptyStateHeading('Nicio fișă de lucru') + ->emptyStateDescription('Crează prima fișă pentru o mașină existentă. Adaugă manopere, piese, plăți — totalul se calculează automat.') + ->emptyStateIcon('heroicon-o-wrench-screwdriver') ->defaultSort('opened_at', 'desc'); } diff --git a/app/Http/Middleware/RequireOnboarding.php b/app/Http/Middleware/RequireOnboarding.php new file mode 100644 index 0000000..25991f3 --- /dev/null +++ b/app/Http/Middleware/RequireOnboarding.php @@ -0,0 +1,56 @@ +path(); + + // Skip non-tenant-panel paths. + if (! str_starts_with($path, 'app')) { + return $next($request); + } + + // Skip auth & onboarding paths. + if ( + str_contains($path, 'login') + || str_contains($path, 'logout') + || str_contains($path, 'onboarding') + || str_starts_with($path, 'livewire') + || str_starts_with($path, 'filament') + ) { + return $next($request); + } + + $tenant = app(TenantManager::class)->current(); + if (! $tenant) { + return $next($request); + } + + // Only redirect authenticated users. + if (! auth('web')->check()) { + return $next($request); + } + + if (empty($tenant->settings['onboarded_at'])) { + return redirect('/app/onboarding'); + } + + return $next($request); + } +} diff --git a/app/Providers/Filament/TenantPanelProvider.php b/app/Providers/Filament/TenantPanelProvider.php index f10f346..d9e9789 100644 --- a/app/Providers/Filament/TenantPanelProvider.php +++ b/app/Providers/Filament/TenantPanelProvider.php @@ -70,6 +70,7 @@ class TenantPanelProvider extends PanelProvider AddQueuedCookiesToResponse::class, StartSession::class, \App\Http\Middleware\SetLocale::class, + \App\Http\Middleware\RequireOnboarding::class, AuthenticateSession::class, ShareErrorsFromSession::class, VerifyCsrfToken::class, diff --git a/resources/views/filament/tenant/pages/onboarding.blade.php b/resources/views/filament/tenant/pages/onboarding.blade.php new file mode 100644 index 0000000..80b3e4e --- /dev/null +++ b/resources/views/filament/tenant/pages/onboarding.blade.php @@ -0,0 +1,77 @@ + + + +
+
+

👋 Bun venit!

+ Hai să facem 30 de secunde de configurare ca AutoCRM-ul tău să arate exact cum vrei tu. + Toate aceste setări pot fi schimbate oricând din meniul „Setări". +
+ + {{-- Stepper --}} +
+ @for ($i = 1; $i <= 3; $i++) +
+ {{ $step > $i ? '✓' : $i }} +
+ @if ($i < 3) +
+ @endif + @endfor +
+ +
+ {{ $this->form }} + +
+
+ @if ($step > 1) + + @endif +
+
+ @if ($step < 3) + + @else + + @endif +
+
+
+
+