Deploy 1: i18n + Notifications + Global Search + Tests

- SetLocale middleware (ro/ru/en, session-first, user-persisted)
- Lang switcher in topbar (Filament render hook USER_MENU_BEFORE)
- POST /locale/{lang} route persists to user.locale + session
- Database notifications enabled on tenant panel (30s polling)
- GlobalSearch (Cmd+K / Ctrl+K) on Client, Vehicle, WorkOrder, Lead, Part
- Tests: TenantIsolation (4), AuthFlow (2), WorkOrderCalc (3), MarkupRule (3)
This commit is contained in:
2026-05-07 18:22:48 +00:00
parent 6c72fc7db1
commit d1e0695930
17 changed files with 770 additions and 0 deletions
@@ -26,6 +26,21 @@ class ClientResource extends Resource
protected static ?int $navigationSort = 10;
protected static ?string $recordTitleAttribute = 'name';
public static function getGloballySearchableAttributes(): array
{
return ['name', 'phone', 'phone_alt', 'email', 'company_name'];
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
'Telefon' => $record->phone,
'Status' => $record->status,
];
}
public static function form(Schema $schema): Schema
{
return $schema->components([
@@ -30,6 +30,19 @@ class LeadResource extends Resource
protected static ?int $navigationSort = 5;
public static function getGloballySearchableAttributes(): array
{
return ['name', 'phone', 'email', 'channel'];
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
'Telefon' => $record->phone,
'Status' => $record->status,
];
}
public static function form(Schema $schema): Schema
{
return $schema->components([
@@ -29,6 +29,19 @@ class PartResource extends Resource
protected static ?int $navigationSort = 40;
public static function getGloballySearchableAttributes(): array
{
return ['name', 'sku', 'brand', 'category'];
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
'Stoc' => (int) $record->stock . ' ' . ($record->unit ?? 'buc'),
'Preț' => number_format((float) $record->sell_price, 2),
];
}
public static function getNavigationBadge(): ?string
{
$low = static::getModel()::query()
@@ -27,6 +27,24 @@ class VehicleResource extends Resource
protected static ?int $navigationSort = 20;
public static function getGloballySearchableAttributes(): array
{
return ['plate', 'vin', 'brand', 'model'];
}
public static function getGlobalSearchResultTitle(\Illuminate\Database\Eloquent\Model $record): string
{
return trim(($record->brand ?? '') . ' ' . ($record->model ?? '') . ' — ' . ($record->plate ?? $record->vin ?? '?'));
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
'Client' => $record->client?->name ?? '—',
'An' => $record->year ?? '—',
];
}
public static function form(Schema $schema): Schema
{
return $schema->components([
@@ -34,6 +34,25 @@ class WorkOrderResource extends Resource
protected static ?int $navigationSort = 30;
public static function getGloballySearchableAttributes(): array
{
return ['number', 'description', 'vehicle.plate', 'vehicle.vin', 'client.name', 'client.phone'];
}
public static function getGlobalSearchResultTitle(\Illuminate\Database\Eloquent\Model $record): string
{
return '#' . ($record->number ?? $record->id) . ' · ' . ($record->vehicle?->plate ?? '?');
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
'Client' => $record->client?->name ?? '—',
'Status' => $record->status,
'Total' => number_format((float) $record->total, 2),
];
}
public static function form(Schema $schema): Schema
{
return $schema->components([
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Middleware;
use App\Tenancy\TenantManager;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class SetLocale
{
private const SUPPORTED = ['ro', 'ru', 'en'];
public function handle(Request $request, Closure $next)
{
$locale = $this->resolve($request);
App::setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
private function resolve(Request $request): string
{
$session = $request->session()->get('locale');
if ($session && in_array($session, self::SUPPORTED, true)) {
return $session;
}
$user = Auth::user();
if ($user && ! empty($user->locale) && in_array($user->locale, self::SUPPORTED, true)) {
return $user->locale;
}
$tenant = app(TenantManager::class)->current();
$tenantLang = $tenant?->settings['language'] ?? null;
if ($tenantLang && in_array($tenantLang, self::SUPPORTED, true)) {
return $tenantLang;
}
return config('app.locale', 'ro');
}
}
@@ -38,6 +38,9 @@ class TenantPanelProvider extends PanelProvider
'primary' => Color::Blue,
])
->authGuard('web')
->databaseNotifications()
->databaseNotificationsPolling('30s')
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
->discoverResources(in: app_path('Filament/Tenant/Resources'), for: 'App\\Filament\\Tenant\\Resources')
->discoverPages(in: app_path('Filament/Tenant/Pages'), for: 'App\\Filament\\Tenant\\Pages')
->pages([
@@ -58,6 +61,7 @@ class TenantPanelProvider extends PanelProvider
// unauthenticated → endless redirect to /app/login.
ResolveTenant::class,
CheckTenantStatus::class,
\App\Http\Middleware\SetLocale::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
@@ -114,6 +118,68 @@ class TenantPanelProvider extends PanelProvider
</style>
BLADE)
)
->renderHook(
PanelsRenderHook::USER_MENU_BEFORE,
fn (): string => Blade::render(<<<'BLADE'
@php
$locale = app()->getLocale();
$langs = ['ro' => 'RO', 'ru' => 'RU', 'en' => 'EN'];
$csrf = csrf_token();
@endphp
<div class="al-locale" x-data="{ open: false }">
<button type="button" class="al-locale-btn" @click="open = !open" @click.away="open = false">
🌐 {{ $langs[$locale] ?? 'RO' }}
<span class="al-locale-caret"></span>
</button>
<div x-show="open" x-cloak class="al-locale-menu">
@foreach ($langs as $code => $label)
<form method="POST" action="/locale/{{ $code }}">
<input type="hidden" name="_token" value="{{ $csrf }}">
<button type="submit" class="al-locale-item {{ $locale === $code ? 'active' : '' }}">
{{ $label }}
<span style="opacity:.7;font-size:11px;">
@switch($code)
@case('ro') Română @break
@case('ru') Русский @break
@case('en') English @break
@endswitch
</span>
</button>
</form>
@endforeach
</div>
</div>
<style>
.al-locale { position: relative; margin-right: 8px; }
.al-locale-btn {
background: transparent; border: 1px solid rgba(0,0,0,.08);
border-radius: 8px; padding: 6px 10px; font-size: 12px;
cursor: pointer; color: inherit; display: flex; align-items: center; gap: 4px;
}
.dark .al-locale-btn { border-color: rgba(255,255,255,.1); }
.al-locale-btn:hover { background: rgba(0,0,0,.04); }
.dark .al-locale-btn:hover { background: rgba(255,255,255,.04); }
.al-locale-caret { font-size: 10px; opacity: .6; }
.al-locale-menu {
position: absolute; top: 100%; right: 0; margin-top: 4px;
background: #fff; border: 1px solid #e5e7eb; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,.08);
min-width: 160px; z-index: 50; overflow: hidden;
}
.dark .al-locale-menu { background: #1f2937; border-color: #374151; }
.al-locale-menu form { margin: 0; }
.al-locale-item {
display: block; width: 100%; text-align: left;
background: transparent; border: none; padding: 8px 12px;
font-size: 13px; cursor: pointer; color: inherit;
}
.al-locale-item:hover { background: #f3f4f6; }
.dark .al-locale-item:hover { background: #374151; }
.al-locale-item.active { background: #eff6ff; color: #2563eb; font-weight: 600; }
.dark .al-locale-item.active { background: #1e3a8a; color: #93c5fd; }
</style>
BLADE)
)
->renderHook(
PanelsRenderHook::SIDEBAR_LOGO_BEFORE,
fn (): string => Blade::render(<<<'BLADE'