diff --git a/app/Filament/Tenant/Pages/Settings.php b/app/Filament/Tenant/Pages/Settings.php index 5f2c47d..45f14fb 100644 --- a/app/Filament/Tenant/Pages/Settings.php +++ b/app/Filament/Tenant/Pages/Settings.php @@ -60,6 +60,8 @@ class Settings extends Page 'services' => isset($settings['services']) ? implode(', ', (array) $settings['services']) : '', 'cars' => isset($settings['cars']) ? implode(', ', (array) $settings['cars']) : '', 'faq_video_url' => $settings['faq_video_url'] ?? null, + 'nav_group_order' => collect($settings['nav_group_order'] ?? \App\Http\Middleware\ConfigureNavGroups::DEFAULT_ORDER) + ->map(fn ($k) => ['key' => $k])->all(), 'notify_wo_ready' => $notify['wo_ready'] ?? true, 'notify_payment' => $notify['payment'] ?? true, 'notify_appointment' => $notify['appointment'] ?? true, @@ -155,6 +157,28 @@ class Settings extends Page ->placeholder('https://www.youtube.com/watch?v=... sau https://vimeo.com/...') ->helperText(__('Afișat în pagina FAQ ca player embedded. Lasă gol pentru a-l ascunde.')), ]), + Schemas\Components\Section::make(__('Ordine meniu lateral')) + ->description(__('Trage grupurile pentru a schimba ordinea lor în sidebar-ul stâng.')) + ->columns(1) + ->schema([ + Forms\Components\Repeater::make('nav_group_order') + ->label('') + ->schema([ + Forms\Components\Select::make('key') + ->label(__('Grup')) + ->options(array_combine( + \App\Http\Middleware\ConfigureNavGroups::DEFAULT_ORDER, + array_map(fn ($k) => __('nav.group.' . $k), \App\Http\Middleware\ConfigureNavGroups::DEFAULT_ORDER), + )) + ->required() + ->distinct(), + ]) + ->reorderable() + ->reorderableWithButtons() + ->addable(false) + ->deletable(false) + ->columnSpanFull(), + ]), Schemas\Components\Section::make(__('Logo & favicon')) ->columns(2) ->schema([ @@ -275,6 +299,8 @@ class Settings extends Page 'services' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['services'] ?? ''))))), 'cars' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['cars'] ?? ''))))), 'faq_video_url' => trim((string) ($data['faq_video_url'] ?? '')) ?: null, + 'nav_group_order' => collect($data['nav_group_order'] ?? []) + ->pluck('key')->filter()->values()->all() ?: null, 'notify' => [ 'wo_ready' => (bool) ($data['notify_wo_ready'] ?? true), 'payment' => (bool) ($data['notify_payment'] ?? true), diff --git a/app/Http/Middleware/ConfigureNavGroups.php b/app/Http/Middleware/ConfigureNavGroups.php new file mode 100644 index 0000000..5a163f9 --- /dev/null +++ b/app/Http/Middleware/ConfigureNavGroups.php @@ -0,0 +1,52 @@ +current(); + if (! $tenant) return $next($request); + + $order = (array) ($tenant->settings['nav_group_order'] ?? []); + if (empty($order)) $order = self::DEFAULT_ORDER; + + // Only touch the tenant panel — bail if the current request isn't in it. + $panel = Filament::getCurrentPanel(); + if (! $panel || $panel->getId() !== 'tenant') return $next($request); + + $groups = array_map( + fn (string $key) => NavigationGroup::make(__('nav.group.' . $key)), + $order, + ); + $panel->navigationGroups($groups); + + return $next($request); + } +} diff --git a/app/Support/Hints.php b/app/Support/Hints.php index 3092cb2..b09f74e 100644 --- a/app/Support/Hints.php +++ b/app/Support/Hints.php @@ -21,10 +21,14 @@ namespace App\Support; class Hints { public const AREAS = [ - 'service' => ['label' => ['ro' => 'Fișe lucru', 'ru' => 'Заказ-наряды', 'en' => 'Work orders'], 'icon' => '🔧'], - 'crm' => ['label' => ['ro' => 'CRM & Clienți', 'ru' => 'CRM и клиенты', 'en' => 'CRM & Customers'], 'icon' => '👥'], - 'depozit' => ['label' => ['ro' => 'Depozit & Piese', 'ru' => 'Склад и запчасти', 'en' => 'Warehouse & Parts'], 'icon' => '📦'], - 'finante' => ['label' => ['ro' => 'Finanțe', 'ru' => 'Финансы', 'en' => 'Finance'], 'icon' => '💰'], + 'service' => ['label' => ['ro' => 'Service (fișe lucru)', 'ru' => 'Сервис (заказы)', 'en' => 'Service (work orders)'], 'icon' => '🔧', 'color' => '#0ea5e9'], + 'crm' => ['label' => ['ro' => 'CRM & Clienți', 'ru' => 'CRM и клиенты', 'en' => 'CRM & Customers'], 'icon' => '👥', 'color' => '#8b5cf6'], + 'depozit' => ['label' => ['ro' => 'Depozit & Piese', 'ru' => 'Склад и запчасти', 'en' => 'Warehouse & Parts'], 'icon' => '📦', 'color' => '#f59e0b'], + 'finante' => ['label' => ['ro' => 'Finanțe', 'ru' => 'Финансы', 'en' => 'Finance'], 'icon' => '💰', 'color' => '#10b981'], + 'analiza' => ['label' => ['ro' => 'Analiză & Rapoarte', 'ru' => 'Аналитика и отчёты', 'en' => 'Analytics & Reports'], 'icon' => '📊', 'color' => '#ec4899'], + 'marketing' => ['label' => ['ro' => 'Marketing', 'ru' => 'Маркетинг', 'en' => 'Marketing'], 'icon' => '📣', 'color' => '#f97316'], + 'magazin' => ['label' => ['ro' => 'Magazin online', 'ru' => 'Онлайн-магазин', 'en' => 'Online shop'], 'icon' => '🛒', 'color' => '#06b6d4'], + 'admin' => ['label' => ['ro' => 'Administrare', 'ru' => 'Администрирование', 'en' => 'Administration'], 'icon' => '⚙️', 'color' => '#64748b'], ]; /** @return array */ @@ -419,7 +423,7 @@ class Hints // Magazin 'shop.orders.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Comenzi online (magazin)', 'ru' => 'Онлайн-заказы (магазин)', 'en' => 'Online orders (shop)'], + 'area' => 'magazin', 'title' => ['ro' => 'Comenzi online (magazin)', 'ru' => 'Онлайн-заказы (магазин)', 'en' => 'Online orders (shop)'], 'body' => [ 'ro' => 'Comenzi primite din magazin-ul public (subdomeniu tenant). Poți accepta/refuza, marca livrat, genera factură. Stocul scade automat la livrare.', 'ru' => 'Заказы из публичного магазина (тенант-поддомен). Принять/отклонить, отметить доставку, счёт. Остаток списывается при доставке.', @@ -427,7 +431,7 @@ class Hints ], ], 'shop.customers.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Clienți magazin', 'ru' => 'Клиенты магазина', 'en' => 'Shop customers'], + 'area' => 'magazin', 'title' => ['ro' => 'Clienți magazin', 'ru' => 'Клиенты магазина', 'en' => 'Shop customers'], 'body' => [ 'ro' => 'Clienți care s-au înregistrat pe magazin-ul online (separați de baza CRM service). Poți converti într-un client CRM cu 1 click atunci când vine în service.', 'ru' => 'Клиенты онлайн-магазина (отдельно от CRM-базы). При приезде в сервис — 1 клик → CRM-клиент.', @@ -437,7 +441,7 @@ class Hints // Marketing 'marketing.channels.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Canale marketing', 'ru' => 'Маркетинговые каналы', 'en' => 'Marketing channels'], + 'area' => 'marketing', 'title' => ['ro' => 'Canale marketing', 'ru' => 'Маркетинговые каналы', 'en' => 'Marketing channels'], 'body' => [ 'ro' => 'Sursele de leaduri: Google Ads, Facebook, recomandare, walk-in, telefon direct. Fiecare cerere se atribuie unui canal → vezi în Rapoarte care canal aduce cel mai bun ROI.', 'ru' => 'Источники лидов: Google Ads, Facebook, рекомендация, walk-in, звонок. Каждая заявка привязывается к каналу → в Отчётах видишь ROI по каналу.', @@ -445,7 +449,7 @@ class Hints ], ], 'marketing.templates.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Template-uri mesaje', 'ru' => 'Шаблоны сообщений', 'en' => 'Message templates'], + 'area' => 'marketing', 'title' => ['ro' => 'Template-uri mesaje', 'ru' => 'Шаблоны сообщений', 'en' => 'Message templates'], 'body' => [ 'ro' => 'Mesaje pre-scrise pentru trimitere rapidă către client: „Fișa e gata", „Confirmare programare", „Ofertă piesă". Suportă variabile ({client.name}, {wo.number}, {eta}). Se folosesc din Chat client sau notificări automate.', 'ru' => 'Готовые сообщения: «Заказ готов», «Подтверждение записи», «Оферта на запчасть». Поддерживают переменные ({client.name}, {wo.number}, {eta}).', @@ -455,7 +459,7 @@ class Hints // Analiză 'analytics.recommendations.overview' => [ - 'area' => 'finante', 'title' => ['ro' => 'Recomandări automate', 'ru' => 'Автоматические рекомендации', 'en' => 'Auto recommendations'], + 'area' => 'analiza', 'title' => ['ro' => 'Recomandări automate', 'ru' => 'Автоматические рекомендации', 'en' => 'Auto recommendations'], 'body' => [ 'ro' => 'Sistemul analizează activitatea și îți spune: „Piese cu stoc mort de X luni", „Manopere neprofitabile", „Clienți inactivi > 6 luni". Fiecare are un buton de acțiune (ex: „Trimite ofertă").', 'ru' => 'Система анализирует и подсказывает: «Мёртвый сток X месяцев», «Убыточные работы», «Клиенты 6+ мес. без визитов». Кнопка действия рядом (напр. «Отправить оффер»).', @@ -463,7 +467,7 @@ class Hints ], ], 'analytics.ai.overview' => [ - 'area' => 'finante', 'title' => ['ro' => 'Asistent AI', 'ru' => 'AI-ассистент', 'en' => 'AI assistant'], + 'area' => 'analiza', 'title' => ['ro' => 'Asistent AI', 'ru' => 'AI-ассистент', 'en' => 'AI assistant'], 'body' => [ 'ro' => 'Întreabă în limbaj natural: „Cât am câștigat luna trecută?", „Care e piesa cea mai vândută la BMW-uri?", „Ce mecanic e cel mai productiv?". AI-ul răspunde din datele tale, cu link-uri către raportul relevant.', 'ru' => 'Спрашивай на естественном языке: «Сколько заработал в прошлом месяце?», «Какая запчасть чаще продаётся на BMW?», «Кто самый продуктивный механик?». AI отвечает по твоим данным + ссылки на отчёты.', @@ -471,7 +475,7 @@ class Hints ], ], 'analytics.workload.overview' => [ - 'area' => 'finante', 'title' => ['ro' => 'Încărcare STO', 'ru' => 'Загрузка СТО', 'en' => 'Workshop load'], + 'area' => 'analiza', 'title' => ['ro' => 'Încărcare STO', 'ru' => 'Загрузка СТО', 'en' => 'Workshop load'], 'body' => [ 'ro' => 'Grafic pe post × zi cu ore rezervate + capacitate. Roșu = supra-încărcat, verde = liber. Ideal pentru planificarea programărilor și evitarea aglomerării unui maistru.', 'ru' => 'График по посту × день: занятые часы + мощность. Красный — перегруз, зелёный — свободно. Помогает планировать записи.', @@ -481,7 +485,7 @@ class Hints // Admin 'admin.users.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Utilizatori (echipa)', 'ru' => 'Пользователи (команда)', 'en' => 'Users (team)'], + 'area' => 'admin', 'title' => ['ro' => 'Utilizatori (echipa)', 'ru' => 'Пользователи (команда)', 'en' => 'Users (team)'], 'body' => [ 'ro' => 'Invită oameni pe email — primesc link de acceptare + parolă temporară. Alocare rol (owner/admin/master/accountant/receptionist) → rolul definește ce văd și ce pot modifica. Dezactivarea unui user îl blochează fără să șterge datele.', 'ru' => 'Приглашай по email — пользователь получает ссылку + временный пароль. Роль (owner/admin/master/accountant/receptionist) — определяет права. Деактивация блокирует без удаления данных.', @@ -489,7 +493,7 @@ class Hints ], ], 'admin.roles.overview' => [ - 'area' => 'crm', 'title' => ['ro' => 'Roluri & drepturi', 'ru' => 'Роли и права', 'en' => 'Roles & permissions'], + 'area' => 'admin', 'title' => ['ro' => 'Roluri & drepturi', 'ru' => 'Роли и права', 'en' => 'Roles & permissions'], 'body' => [ 'ro' => 'Fiecare rol are un set de permisiuni granulare (poate vedea marja internă? poate șterge fișe? poate exporta rapoarte?). Owner-ul are toate. Poți crea roluri custom pentru cazuri speciale.', 'ru' => 'У каждой роли — набор гранулярных прав (видит внутреннюю маржу? может удалять? экспортировать?). У Owner — все. Можно создать кастомные роли.', @@ -497,7 +501,7 @@ class Hints ], ], 'admin.posts.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'Posturi de lucru', 'ru' => 'Посты (рабочие места)', 'en' => 'Work posts'], + 'area' => 'admin', 'title' => ['ro' => 'Posturi de lucru', 'ru' => 'Посты (рабочие места)', 'en' => 'Work posts'], 'body' => [ 'ro' => 'Locurile fizice unde se face lucrarea: „Elevator 1", „Elevator 2", „Zona geometrie", „Vopsitorie". Fiecare post are un maistru default (opțional). Se folosesc în Calendar pentru gruparea programărilor.', 'ru' => 'Физические места работ: «Подъёмник 1», «Развал-схождение», «Покраска». У поста может быть мастер по умолчанию. Используются в Календаре для группировки.', @@ -505,7 +509,7 @@ class Hints ], ], 'admin.settings.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'Setări service', 'ru' => 'Настройки сервиса', 'en' => 'Service settings'], + 'area' => 'admin', 'title' => ['ro' => 'Setări service', 'ru' => 'Настройки сервиса', 'en' => 'Service settings'], 'body' => [ 'ro' => 'Config-ul general al service-ului: nume, logo, culoare temă, program de lucru, adresă, telefon public, moneda, contul bancar pentru facturi. Modificările sunt vizibile imediat pentru toți userii.', 'ru' => 'Общий конфиг сервиса: имя, лого, цвет темы, график, адрес, телефон, валюта, банк для счетов. Изменения видны всем сразу.', @@ -513,7 +517,7 @@ class Hints ], ], 'admin.integrations.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'Integrări (Telegram/WhatsApp/Google/Facebook)', 'ru' => 'Интеграции', 'en' => 'Integrations'], + 'area' => 'admin', 'title' => ['ro' => 'Integrări (Telegram/WhatsApp/Google/Facebook)', 'ru' => 'Интеграции', 'en' => 'Integrations'], 'body' => [ 'ro' => 'Activează canale: Telegram bot (token de la @BotFather), WhatsApp Business (token Facebook), Google Ads, Facebook Lead Ads. După activare, mesajele/lead-urile intră automat în CRM.', 'ru' => 'Активируй каналы: Telegram-бот (токен от @BotFather), WhatsApp Business (Facebook), Google Ads, Facebook Lead Ads. После активации всё уходит в CRM автоматом.', @@ -521,7 +525,7 @@ class Hints ], ], 'admin.tokens.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'API Tokens', 'ru' => 'API-токены', 'en' => 'API tokens'], + 'area' => 'admin', 'title' => ['ro' => 'API Tokens', 'ru' => 'API-токены', 'en' => 'API tokens'], 'body' => [ 'ro' => 'Token-uri pentru integrări externe (aplicații mobile, script-uri, terți). Fiecare token are un nume + drepturi. Tokenul se afișează O SINGURĂ DATĂ la creare — copiază-l imediat.', 'ru' => 'Токены для внешних интеграций (мобильные приложения, скрипты). У каждого — имя + права. Токен показывается ОДИН РАЗ при создании — копируй сразу.', @@ -529,7 +533,7 @@ class Hints ], ], 'admin.backup.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'Backup date', 'ru' => 'Резервные копии', 'en' => 'Data backup'], + 'area' => 'admin', 'title' => ['ro' => 'Backup date', 'ru' => 'Резервные копии', 'en' => 'Data backup'], 'body' => [ 'ro' => 'Descarcă un snapshot complet al datelor tenantului (DB + fișiere media) sub formă de arhivă. Recomandat: o dată pe lună. Se poate importa înapoi manual (contactează suport).', 'ru' => 'Скачивай снапшот всех данных (БД + медиа) архивом. Рекомендуется раз в месяц. Восстановление — через поддержку.', @@ -537,7 +541,7 @@ class Hints ], ], 'admin.activity.overview' => [ - 'area' => 'service', 'title' => ['ro' => 'Jurnal activitate', 'ru' => 'Журнал активности', 'en' => 'Activity log'], + 'area' => 'admin', 'title' => ['ro' => 'Jurnal activitate', 'ru' => 'Журнал активности', 'en' => 'Activity log'], 'body' => [ 'ro' => 'Audit-log complet: cine, ce, când. Utile pentru investigație (ex: „cine a șters fișa X?"). Filtrează pe user, pe entitate, pe interval. Se păstrează 12 luni.', 'ru' => 'Полный аудит: кто, что, когда. Полезно для расследований («кто удалил заказ X?»). Фильтры: пользователь, сущность, период. Хранится 12 месяцев.', diff --git a/bootstrap/app.php b/bootstrap/app.php index eddd995..3014b44 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -31,6 +31,7 @@ return Application::configure(basePath: dirname(__DIR__)) \App\Http\Middleware\ResolveTenant::class, \App\Http\Middleware\CheckTenantStatus::class, \App\Http\Middleware\SetLocale::class, + \App\Http\Middleware\ConfigureNavGroups::class, ]); // API routes also need tenant resolution by host so the same diff --git a/lang/en.json b/lang/en.json index 013eb83..b1ddc7c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -949,6 +949,7 @@ "Grafic": "Chart", "Gravitate": "Severity", "Gravă": "Severe", + "Grup": "Group", "Grupare:": "Group:", "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet": "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet", "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet\") și aplică-l pe o fișă cu un click.": "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet\") și aplică-l pe o fișă cu un click.", @@ -1428,6 +1429,7 @@ "Order number": "Order number", "Orders": "Orders", "Ordine": "Order", + "Ordine meniu lateral": "Sidebar menu order", "Ordonare": "Sort", "Ordonat": "Sorted", "Ordonează după": "Sort by", @@ -2105,6 +2107,7 @@ "Tracking client": "Client tracking", "Tracțiune": "Drive", "Trage fișierul aici": "Drop file here", + "Trage grupurile pentru a schimba ordinea lor în sidebar-ul stâng.": "Drag groups to change their order in the left sidebar.", "Transfer": "Transfer", "Transfer bancar": "Bank transfer", "Transfer bancar: pune doar IBAN-ul tău, fără cont gateway": "Transfer bancar: pune doar IBAN-ul tău, fără cont gateway", diff --git a/lang/ru.json b/lang/ru.json index 5e91f51..2ea2476 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -949,6 +949,7 @@ "Grafic": "График", "Gravitate": "Тяжесть", "Gravă": "Тяжёлое", + "Grup": "Группа", "Grupare:": "Группировка:", "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet": "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet", "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet\") și aplică-l pe o fișă cu un click.": "Grupează manopere + piese frecvente într-un șablon (ex: „Schimb ulei complet\") și aplică-l pe o fișă cu un click.", @@ -1428,6 +1429,7 @@ "Order number": "№ заказа", "Orders": "Заказы", "Ordine": "Порядок", + "Ordine meniu lateral": "Порядок бокового меню", "Ordonare": "Сортировка", "Ordonat": "Отсортировано", "Ordonează după": "Сортировать по", @@ -2105,6 +2107,7 @@ "Tracking client": "Отслеживание клиента", "Tracțiune": "Привод", "Trage fișierul aici": "Перетащите файл сюда", + "Trage grupurile pentru a schimba ordinea lor în sidebar-ul stâng.": "Перетащи группы, чтобы изменить порядок в боковом меню слева.", "Transfer": "Перевод", "Transfer bancar": "Банковский перевод", "Transfer bancar: pune doar IBAN-ul tău, fără cont gateway": "Transfer bancar: pune doar IBAN-ul tău, fără cont gateway", diff --git a/resources/views/filament/tenant/pages/faq.blade.php b/resources/views/filament/tenant/pages/faq.blade.php index cca6f9a..dcdde8d 100644 --- a/resources/views/filament/tenant/pages/faq.blade.php +++ b/resources/views/filament/tenant/pages/faq.blade.php @@ -7,13 +7,7 @@ $totalHints = collect($grouped)->sum(fn ($g) => count($g)); $videoEmbed = $this->getVideoEmbedUrl(); $videoRaw = $this->getVideoRawUrl(); - // Distinct accent color per area for card headers - $areaColors = [ - 'service' => '#0ea5e9', - 'crm' => '#8b5cf6', - 'depozit' => '#f59e0b', - 'finante' => '#10b981', - ]; + // Read accent color from AREAS registry (single source of truth) @endphp