From 06081159b66623d937670bf5aa59da64695b655b Mon Sep 17 00:00:00 2001 From: Vasyka Date: Tue, 1 Sep 2026 09:46:07 +0000 Subject: [PATCH] feat(hints): contextual help system + FAQ page (MVP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastructure for a portal-wide in-app help system: * Users have hints_enabled (default true) and dismissed_hints (JSON array) columns. User::shouldSeeHint(key) checks both. * app/Support/Hints.php — single-source-of-truth registry with 14 pilot hints across Service (5), CRM (3), Depozit (3), Finanțe (3). Each entry has RO/RU/EN title + body + optional next-step + links. * Blade component renders a small "?" icon with Alpine.js popover; the popover shows title, body, next-step, related links and an "X" button that POSTs to /app/hints/{key}/dismiss. * HintController handles dismiss (per key), toggle (global on/off) and reset (clear dismissed + re-enable). Routes are auth:web. * /app/faq page (Filament Page under Admin group) renders the whole registry grouped by area with a live search box and buttons to toggle global hints or reset dismissed ones. * Wired 3 pilot hints into the WO dashboard: title (overview), Docs tab PDF preview, and the Chat client card. Follow-ups: extend registry to cover more pages and add tags where useful. Filament resource fields can also reuse the same copy via ->hint()/->helperText(). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Filament/Tenant/Pages/Faq.php | 64 +++++ app/Http/Controllers/HintController.php | 42 +++ app/Models/Tenant/User.php | 10 + app/Support/Hints.php | 271 ++++++++++++++++++ ...6_09_01_000001_add_hint_prefs_to_users.php | 23 ++ lang/en.json | 7 + lang/en/nav.php | 1 + lang/ro/nav.php | 1 + lang/ru.json | 7 + lang/ru/nav.php | 1 + resources/views/components/hint.blade.php | 37 +++ .../views/filament/tenant/pages/faq.blade.php | 83 ++++++ .../pages/work-order-dashboard.blade.php | 5 +- routes/web.php | 9 + 14 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 app/Filament/Tenant/Pages/Faq.php create mode 100644 app/Http/Controllers/HintController.php create mode 100644 app/Support/Hints.php create mode 100644 database/migrations/2026_09_01_000001_add_hint_prefs_to_users.php create mode 100644 resources/views/components/hint.blade.php create mode 100644 resources/views/filament/tenant/pages/faq.blade.php diff --git a/app/Filament/Tenant/Pages/Faq.php b/app/Filament/Tenant/Pages/Faq.php new file mode 100644 index 0000000..5319b29 --- /dev/null +++ b/app/Filament/Tenant/Pages/Faq.php @@ -0,0 +1,64 @@ +search)); + $lc = Hints::locale(); + $grouped = Hints::byArea(); + if ($q === '') return $grouped; + $out = []; + foreach ($grouped as $area => $hints) { + $matched = array_filter($hints, function ($h) use ($q, $lc) { + $t = mb_strtolower(($h['title'][$lc] ?? '') . ' ' . ($h['body'][$lc] ?? '')); + return str_contains($t, $q); + }); + if (! empty($matched)) $out[$area] = $matched; + } + return $out; + } +} diff --git a/app/Http/Controllers/HintController.php b/app/Http/Controllers/HintController.php new file mode 100644 index 0000000..b91a11b --- /dev/null +++ b/app/Http/Controllers/HintController.php @@ -0,0 +1,42 @@ +user(); + abort_unless($user, 401); + $dismissed = (array) ($user->dismissed_hints ?? []); + if (! in_array($key, $dismissed, true)) { + $dismissed[] = $key; + $user->dismissed_hints = $dismissed; + $user->save(); + } + return response()->json(['ok' => true]); + } + + /** Toggle global hints on/off from the profile / settings page. */ + public function toggle(Request $request) + { + $user = $request->user(); + abort_unless($user, 401); + $user->hints_enabled = ! $user->hints_enabled; + $user->save(); + return back(); + } + + /** Bring back all dismissed hints (used from FAQ / profile "resetează hint-uri"). */ + public function reset(Request $request) + { + $user = $request->user(); + abort_unless($user, 401); + $user->dismissed_hints = []; + $user->hints_enabled = true; + $user->save(); + return back(); + } +} diff --git a/app/Models/Tenant/User.php b/app/Models/Tenant/User.php index 197fd3f..24b679d 100644 --- a/app/Models/Tenant/User.php +++ b/app/Models/Tenant/User.php @@ -31,6 +31,7 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication protected $fillable = [ 'company_id', 'name', 'email', 'phone', 'avatar_url', 'role', 'status', 'locale', + 'hints_enabled', 'dismissed_hints', 'specialization', 'color', 'hourly_rate', 'internal_margin_pct', 'email_verified_at', 'password', 'last_login_at', 'email_authentication_at', @@ -53,9 +54,18 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication 'password' => 'hashed', 'app_authentication_secret' => 'encrypted', 'app_authentication_recovery_codes' => 'encrypted:array', + 'hints_enabled' => 'boolean', + 'dismissed_hints' => 'array', ]; } + /** True when the user has hints on globally AND this specific key isn't dismissed. */ + public function shouldSeeHint(string $key): bool + { + if (! $this->hints_enabled) return false; + return ! in_array($key, (array) ($this->dismissed_hints ?? []), true); + } + public function canAccessPanel(Panel $panel): bool { return $panel->getId() === 'tenant' diff --git a/app/Support/Hints.php b/app/Support/Hints.php new file mode 100644 index 0000000..0cd0ced --- /dev/null +++ b/app/Support/Hints.php @@ -0,0 +1,271 @@ + Blade component renders a single entry as a "?" popover. + * + * Add hints incrementally — this file is the single source of truth. + */ +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' => '💰'], + ]; + + /** @return array */ + public static function all(): array + { + return [ + // ─── Service / Work orders ──────────────────────────────── + 'wo.list.overview' => [ + 'area' => 'service', + 'title' => [ + 'ro' => 'Lista fișelor de lucru', + 'ru' => 'Список заказ-нарядов', + 'en' => 'Work orders list', + ], + 'body' => [ + 'ro' => 'Aici vezi toate fișele active. Click pe un rând deschide dashboard-ul fișei (Mitchell1-style). Butonul „+ Fișă nouă" creează una nouă și te duce direct pe dashboard. Filtrele de sus permit selectare pe status, plată, maistru.', + 'ru' => 'Здесь все активные заказы. Клик по строке открывает дашборд заказа. Кнопка «+ Новый заказ» создаёт и сразу открывает дашборд. Фильтры вверху — статус, оплата, мастер.', + 'en' => 'All active work orders. Row click opens the Mitchell1-style dashboard. "+ New" creates and lands on the dashboard. Top filters: status, payment, master.', + ], + 'links' => [ + ['label_ro' => 'Deschide un dashboard', 'label_ru' => 'Открыть дашборд', 'label_en' => 'Open a dashboard', 'url' => '/app/work-orders'], + ], + 'next' => [ + 'ro' => 'Click pe orice rând sau „+ Fișă nouă"', + 'ru' => 'Клик по строке или «+ Новый заказ»', + 'en' => 'Click any row or "+ New"', + ], + ], + 'wo.dashboard.overview' => [ + 'area' => 'service', + 'title' => [ + 'ro' => 'Dashboard fișă (hub central)', + 'ru' => 'Дашборд заказа (центр управления)', + 'en' => 'Work order dashboard (hub)', + ], + 'body' => [ + 'ro' => 'Toate acțiunile legate de această fișă într-un singur ecran: stânga = client & mașină & istoric reparații, mijloc = lucrări/piese/diagnostic/foto/documente/note (taburi), dreapta = finanțe & timeline & chat client. Jos: navigare între fișe, comandă repetată, închide fișă.', + 'ru' => 'Все действия по заказу в одном экране: слева — клиент, авто, история; в центре — работы/запчасти/диагностика/фото/документы/заметки (табы); справа — финансы, лента, чат с клиентом. Внизу: навигация, повтор заказа, закрытие.', + 'en' => 'Everything for this WO on one screen: left = client + vehicle + repair history; middle = tabs (works/parts/diag/photos/docs/notes); right = finance + timeline + chat. Bottom bar: prev/next, repeat, close WO.', + ], + 'next' => [ + 'ro' => 'Explorează taburile din mijloc: Lucrări, Piese, Foto etc.', + 'ru' => 'Проверьте табы в центре: Работы, Запчасти, Фото и т.д.', + 'en' => 'Try the middle tabs: Works, Parts, Photos, etc.', + ], + ], + 'wo.dashboard.pdf' => [ + 'area' => 'service', + 'title' => [ + 'ro' => 'Vizualizare & descărcare PDF', + 'ru' => 'Просмотр и скачивание PDF', + 'en' => 'PDF preview & download', + ], + 'body' => [ + 'ro' => 'Butonul „Vizualizare PDF" deschide factura într-un modal cu preview embedded. Din modal poți printa (via viewer-ul browserului) sau descărca (link „Descarcă"). Nu forțează descărcare implicit.', + 'ru' => 'Кнопка «Просмотр PDF» открывает счёт в модалке с превью. Оттуда можно печатать (через просмотрщик браузера) или скачать. По умолчанию не скачивается.', + 'en' => 'The "View PDF" button opens the invoice in a modal preview. You can print (via the browser PDF viewer) or download from there. It doesn\'t force download by default.', + ], + ], + 'wo.dashboard.tracking' => [ + 'area' => 'service', + 'title' => [ + 'ro' => 'Link tracking pentru client', + 'ru' => 'Ссылка отслеживания для клиента', + 'en' => 'Client tracking link', + ], + 'body' => [ + 'ro' => 'Fiecare fișă are un token public unic. Butonul „Link tracking" deschide pagina publică pe care o poți trimite clientului prin SMS/WhatsApp/Telegram — vede statusul lucrării, ETA-ul și fotografiile.', + 'ru' => 'У каждого заказа есть публичный токен. Кнопка «Ссылка отслеживания» открывает публичную страницу, которую можно отправить клиенту — он видит статус, ETA, фотографии.', + 'en' => 'Each WO has a unique public token. "Tracking link" opens the customer-facing page you can send via SMS/WhatsApp/Telegram — status, ETA, photos.', + ], + ], + 'wo.dashboard.chat' => [ + 'area' => 'service', + 'title' => [ + 'ro' => 'Chat cu clientul (Telegram/WhatsApp)', + 'ru' => 'Чат с клиентом (Telegram/WhatsApp)', + 'en' => 'Client chat (Telegram/WhatsApp)', + ], + 'body' => [ + 'ro' => 'Trimite mesaje direct din dashboard dacă ai configurat integrarea Telegram sau WhatsApp în Setări → Integrări. Dacă vezi „Mesageria nu este configurată" — mergi la Integrări și completează token-ul.', + 'ru' => 'Отправляйте сообщения из дашборда, если настроен Telegram или WhatsApp в Настройки → Интеграции. Если написано «Мессенджер не настроен» — заполните токен.', + 'en' => 'Send messages from the dashboard when Telegram/WhatsApp is configured under Settings → Integrations. If it says "Messaging not configured" — go set the token.', + ], + 'links' => [ + ['label_ro' => 'Setări integrări', 'label_ru' => 'Настроить интеграции', 'label_en' => 'Configure integrations', 'url' => '/app/integrations'], + ], + ], + + // ─── CRM ────────────────────────────────────────────────── + 'crm.pipeline.overview' => [ + 'area' => 'crm', + 'title' => [ + 'ro' => 'Pipeline — de la lead la fișă', + 'ru' => 'Pipeline — от лида до заказа', + 'en' => 'Pipeline — from lead to WO', + ], + 'body' => [ + 'ro' => 'Fiecare deal trece prin coloane (New → Contactat → Diagnostic → Confirmat → Fișă creată → Închis/Pierdut). Trage-și cardurile între coloane. Când ajunge la „Fișă creată" se generează automat un WO legat de client + mașină.', + 'ru' => 'Каждый deal проходит через колонки (New → Контакт → Диагностика → Подтверждён → Заказ создан → Закрыт/Проигран). Перетаскивайте карточки. На «Заказ создан» автоматически генерируется WO.', + 'en' => 'Each deal moves across columns (New → Contacted → Diagnosed → Confirmed → WO created → Closed/Lost). Drag cards between columns. Reaching "WO created" auto-generates a work order.', + ], + 'next' => [ + 'ro' => 'Trage un card la stânga/dreapta', + 'ru' => 'Перетащите карточку', + 'en' => 'Drag a card left/right', + ], + ], + 'crm.leads.overview' => [ + 'area' => 'crm', + 'title' => [ + 'ro' => 'Cereri (leaduri) — intrare centralizată', + 'ru' => 'Заявки (лиды) — единая точка входа', + 'en' => 'Leads — single inbox', + ], + 'body' => [ + 'ro' => 'Aici ajung toate cererile: formularul de pe site, telefoane, mesaje Telegram, campanii Google/Facebook (dacă e configurat). Convertește-le în client + deal cu un click. Statusul „nou" apare cu galben pe dashboard.', + 'ru' => 'Все входящие заявки: сайт, звонки, Telegram, Google/Facebook кампании. Одним кликом превращаешь в клиента + deal. Статус «новый» подсвечивается жёлтым на дашборде.', + 'en' => 'All incoming requests: website form, calls, Telegram, Google/Facebook (if configured). Convert to client + deal in one click. "New" status shows amber on the dashboard.', + ], + ], + 'crm.calendar.overview' => [ + 'area' => 'crm', + 'title' => [ + 'ro' => 'Calendar programări (5 vederi)', + 'ru' => 'Календарь записей (5 видов)', + 'en' => 'Appointments calendar (5 views)', + ], + 'body' => [ + 'ro' => 'Zi / Săptămână / Lună / Custom / Listă — comută cu butoanele de sus. Grupare pe post de lucru sau pe maistru. Trage evenimentele pentru re-programare. Butonul „🖨 PDF programări" exportă perioada curentă.', + 'ru' => 'День / Неделя / Месяц / Свой / Список — кнопки сверху. Группировка по посту или мастеру. Перетаскивайте события. «🖨 PDF записей» экспортирует текущий период.', + 'en' => 'Day / Week / Month / Custom / List — top buttons. Group by post or master. Drag events to reschedule. "🖨 PDF" exports the current period.', + ], + ], + + // ─── Depozit ────────────────────────────────────────────── + 'depozit.parts.overview' => [ + 'area' => 'depozit', + 'title' => [ + 'ro' => 'Piese — catalog + stoc', + 'ru' => 'Запчасти — каталог и остатки', + 'en' => 'Parts — catalog + stock', + ], + 'body' => [ + 'ro' => 'Fiecare piesă are cod, preț cost/vânzare, stoc curent, stoc minim și furnizor preferat. Când stocul scade sub minim, apare în widget-ul „Stoc minim atins" pe dashboard. Adaugă la o fișă din tab-ul „Piese" al fișei.', + 'ru' => 'У каждой запчасти: код, цена (закупка/продажа), остаток, минимум, поставщик. Когда остаток ниже минимума — попадает в виджет «Минимальный остаток». Добавляй в заказ через таб «Запчасти».', + 'en' => 'Each part has SKU, cost/sell price, current + min stock, preferred supplier. When stock drops below min, it shows up in the "Low stock" widget. Add to a WO via the WO\'s "Parts" tab.', + ], + ], + 'depozit.excel.import' => [ + 'area' => 'depozit', + 'title' => [ + 'ro' => 'Import factură Excel', + 'ru' => 'Импорт счёта из Excel', + 'en' => 'Excel invoice import', + ], + 'body' => [ + 'ro' => 'Încarcă factura de la furnizor în Excel/CSV. Wizard-ul mapează coloanele (cod, denumire, preț, cant.) și le arată în preview. Piese noi se creează automat, cele existente se actualizează. Maparea coloanelor se salvează per furnizor.', + 'ru' => 'Загрузите счёт поставщика в Excel/CSV. Мастер сопоставит колонки (код, название, цена, кол-во) и покажет превью. Новые запчасти создаются, существующие обновляются. Соответствие колонок сохраняется на поставщика.', + 'en' => 'Upload the supplier invoice in Excel/CSV. The wizard maps columns (SKU, name, price, qty) and shows a preview. New parts are created, existing ones updated. Column mapping is remembered per supplier.', + ], + ], + 'depozit.scanner.usage' => [ + 'area' => 'depozit', + 'title' => [ + 'ro' => 'Scaner barcode', + 'ru' => 'Сканер штрихкодов', + 'en' => 'Barcode scanner', + ], + 'body' => [ + 'ro' => 'Scanează codul de bare al piesei cu telefonul (camera browser) sau cu un scanner USB. Sistemul găsește piesa în catalog și te lasă să o adaugi rapid la o fișă activă sau să ajustezi stocul.', + 'ru' => 'Сканируй штрихкод телефоном (камерой) или USB-сканером. Система найдёт запчасть в каталоге — можно добавить в заказ или изменить остаток.', + 'en' => 'Scan the part barcode with your phone camera or a USB scanner. The system finds it in the catalog — add to an active WO or adjust stock.', + ], + ], + + // ─── Finanțe ────────────────────────────────────────────── + 'finante.payments.overview' => [ + 'area' => 'finante', + 'title' => [ + 'ro' => 'Plăți — încasările tale', + 'ru' => 'Оплаты — приход', + 'en' => 'Payments — cash in', + ], + 'body' => [ + 'ro' => 'Fiecare plată se leagă de o fișă (WO), o metodă (cash, card, transfer) și un user care a primit-o. Suma totală apare pe dashboard-ul principal la „Încasări (luna)" și pe dashboard-ul fișei la „Plătit".', + 'ru' => 'Каждая оплата привязана к заказу, способу (наличные/карта/перевод) и пользователю. Общая сумма — на главном дашборде «Приход (месяц)» и на дашборде заказа «Оплачено».', + 'en' => 'Each payment ties to a WO, a method (cash/card/transfer) and the user who took it. The total shows on the main dashboard ("Cash in / month") and on the WO dashboard ("Paid").', + ], + ], + 'finante.salaries.overview' => [ + 'area' => 'finante', + 'title' => [ + 'ro' => 'Salarii — calcul automat per mecanic', + 'ru' => 'Зарплаты — авторасчёт по механику', + 'en' => 'Salaries — auto per mechanic', + ], + 'body' => [ + 'ro' => 'La sfârșit de perioadă (săptămână/lună), rulează „Închide perioada" în Salarii. Sistemul calculează pentru fiecare mecanic: manoperele făcute × marja internă + bonusuri − avansuri = net de plată. Toate valorile pot fi ajustate manual înainte de export.', + 'ru' => 'В конце периода (неделя/месяц) нажми «Закрыть период» в Зарплатах. Система посчитает по каждому механику: работы × внутренняя маржа + бонусы − авансы = к выплате. Значения можно поправить вручную.', + 'en' => 'At period end (week/month), hit "Close period" in Salaries. The system computes per mechanic: labor × internal margin + bonuses − advances = net pay. All values are editable before export.', + ], + 'links' => [ + ['label_ro' => 'Bonusuri & avansuri', 'label_ru' => 'Бонусы и авансы', 'label_en' => 'Bonuses & advances', 'url' => '/app/payroll-adjustments'], + ], + ], + 'finante.reports.overview' => [ + 'area' => 'finante', + 'title' => [ + 'ro' => 'Rapoarte financiare', + 'ru' => 'Финансовые отчёты', + 'en' => 'Financial reports', + ], + 'body' => [ + 'ro' => 'Vezi aici P&L pe perioadă selectabilă: încasări, cheltuieli, salarii, profit brut, top clienți, top piese vândute, comparație lună-lună. Export CSV pentru contabilitate.', + 'ru' => 'P&L за выбранный период: приход, расход, зарплаты, валовая прибыль, топ клиентов, топ запчастей, сравнение месяцев. Экспорт CSV.', + 'en' => 'P&L for a selectable period: income, expenses, salaries, gross profit, top clients, top parts, month-over-month comparison. CSV export for accounting.', + ], + ], + ]; + } + + public static function get(string $key): ?array + { + return static::all()[$key] ?? null; + } + + /** Group all hints by area, preserving insertion order. */ + public static function byArea(): array + { + $out = []; + foreach (static::all() as $key => $h) { + $out[$h['area']] ??= []; + $out[$h['area']][$key] = $h; + } + return $out; + } + + public static function locale(): string + { + $l = app()->getLocale(); + return in_array($l, ['ro', 'ru', 'en'], true) ? $l : 'ro'; + } +} diff --git a/database/migrations/2026_09_01_000001_add_hint_prefs_to_users.php b/database/migrations/2026_09_01_000001_add_hint_prefs_to_users.php new file mode 100644 index 0000000..b12bca4 --- /dev/null +++ b/database/migrations/2026_09_01_000001_add_hint_prefs_to_users.php @@ -0,0 +1,23 @@ +boolean('hints_enabled')->default(true)->after('locale'); + $t->json('dismissed_hints')->nullable()->after('hints_enabled'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $t) { + $t->dropColumn(['hints_enabled', 'dismissed_hints']); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 6f50852..3429dcb 100644 --- a/lang/en.json +++ b/lang/en.json @@ -32,6 +32,7 @@ ":n poziții importate în Purchase nouă": ":n items imported into a new Purchase", "> 0 calculează automat prețul client.": "> 0 auto-calculates client price.", "A1-03": "A1-03", + "ACTIVE": "ON", "AGRNMD2X": "AGRNMD2X", "AI provider": "AI provider", "AI: preț recomandat": "AI: recommended price", @@ -372,6 +373,7 @@ "Category": "Category", "Caută": "Search", "Caută client, mașină, număr...": "Search client, vehicle, number...", + "Caută în ghid...": "Search the guide...", "Caută în tabel": "Search in table", "Cauza adresării": "Reason for request", "Caz asigurare": "Insurance case", @@ -825,6 +827,7 @@ "Eșapament": "Exhaust", "Ești sigur?": "Are you sure?", "Eșuat": "Failed", + "FAQ & Ghid utilizator": "FAQ & User guide", "FIȘĂ DE LUCRU": "WORK ORDER", "Facturi": "Invoices", "Facturi & abonament": "Facturi & abonament", @@ -952,6 +955,7 @@ "IBAN:": "IBAN:", "ID": "ID", "IDNO / CUI": "IDNO / CUI", + "INACTIVE": "OFF", "INJECTOARE": "INJECTORS", "ITP": "MOT", "Ianuarie": "January", @@ -1281,6 +1285,7 @@ "Nicio piesă montată.": "No parts installed.", "Nicio plată": "No payments", "Nicio plată în perioada selectată.": "No payments in selected period.", + "Nicio potrivire pentru „:q\".": "No matches for \":q\".", "Nicio programare în această perioadă.": "No appointments in this period.", "Nicio programare în perioada selectată.": "No appointments in selected period.", "Niciodată": "Never", @@ -1350,6 +1355,7 @@ "Nu există date": "No data", "Nu există rezultate": "No results", "Nu găsit": "Not found", + "Nu mai arăta": "Don't show again", "Nu pot porni camera: ": "Cannot start camera: ", "Nu re-trimite mai des de X zile": "Don't resend more often than every X days", "Nu te poți șterge pe tine!": "You can't delete yourself!", @@ -1938,6 +1944,7 @@ "Subtotal piese:": "Subtotal piese:", "Succes": "Success", "Sugerează revizie / piese de uzură.": "Suggest service / wear parts.", + "Sugestii pe pagini:": "In-page hints:", "Sumă": "Amount", "Sumă achitată": "Sumă achitată", "Sumă:": "Sumă:", diff --git a/lang/en/nav.php b/lang/en/nav.php index b34fd68..bf14410 100644 --- a/lang/en/nav.php +++ b/lang/en/nav.php @@ -17,6 +17,7 @@ return [ ], 'label' => [ + 'FAQ & Ghid' => 'FAQ & Guide', 'API Tokens' => 'API Tokens', 'Achiziții' => 'Purchases', 'Angajați' => 'Employees', diff --git a/lang/ro/nav.php b/lang/ro/nav.php index 82e66a2..56bc719 100644 --- a/lang/ro/nav.php +++ b/lang/ro/nav.php @@ -19,6 +19,7 @@ return [ // ── Individual navigation labels ── 'label' => [ + 'FAQ & Ghid' => 'FAQ & Ghid', 'API Tokens' => 'API Tokens', 'Achiziții' => 'Achiziții', 'Angajați' => 'Angajați', diff --git a/lang/ru.json b/lang/ru.json index 779f91c..8d4e081 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -32,6 +32,7 @@ ":n poziții importate în Purchase nouă": ":n позиций импортировано в новый заказ", "> 0 calculează automat prețul client.": "> 0 автоматически рассчитывает цену клиента.", "A1-03": "A1-03", + "ACTIVE": "ВКЛ", "AGRNMD2X": "AGRNMD2X", "AI provider": "AI-провайдер", "AI: preț recomandat": "AI: рекомендованная цена", @@ -372,6 +373,7 @@ "Category": "Категория", "Caută": "Найти", "Caută client, mașină, număr...": "Поиск клиента, авто, номера...", + "Caută în ghid...": "Поиск по гиду...", "Caută în tabel": "Поиск в таблице", "Cauza adresării": "Причина обращения", "Caz asigurare": "Страховой случай", @@ -825,6 +827,7 @@ "Eșapament": "Выхлоп", "Ești sigur?": "Ты уверен?", "Eșuat": "Ошибка", + "FAQ & Ghid utilizator": "FAQ и гид пользователя", "FIȘĂ DE LUCRU": "ЗАКАЗ-НАРЯД", "Facturi": "Счета", "Facturi & abonament": "Facturi & abonament", @@ -952,6 +955,7 @@ "IBAN:": "IBAN:", "ID": "ID", "IDNO / CUI": "IDNO / CUI", + "INACTIVE": "ВЫКЛ", "INJECTOARE": "ФОРСУНКИ", "ITP": "ТО", "Ianuarie": "Январь", @@ -1281,6 +1285,7 @@ "Nicio piesă montată.": "Запчасти не установлены.", "Nicio plată": "Нет платежей", "Nicio plată în perioada selectată.": "Нет платежей за выбранный период.", + "Nicio potrivire pentru „:q\".": "Нет совпадений для «:q».", "Nicio programare în această perioadă.": "Нет записей в этом периоде.", "Nicio programare în perioada selectată.": "Нет записей за выбранный период.", "Niciodată": "Никогда", @@ -1350,6 +1355,7 @@ "Nu există date": "Нет данных", "Nu există rezultate": "Нет результатов", "Nu găsit": "Не найдено", + "Nu mai arăta": "Больше не показывать", "Nu pot porni camera: ": "Не удалось запустить камеру: ", "Nu re-trimite mai des de X zile": "Не отправлять повторно чаще чем раз в X дней", "Nu te poți șterge pe tine!": "Нельзя удалить себя!", @@ -1938,6 +1944,7 @@ "Subtotal piese:": "Subtotal piese:", "Succes": "Успех", "Sugerează revizie / piese de uzură.": "Предложите ТО / расходники.", + "Sugestii pe pagini:": "Подсказки на страницах:", "Sumă": "Сумма", "Sumă achitată": "Sumă achitată", "Sumă:": "Sumă:", diff --git a/lang/ru/nav.php b/lang/ru/nav.php index c9443e8..3bfba69 100644 --- a/lang/ru/nav.php +++ b/lang/ru/nav.php @@ -19,6 +19,7 @@ return [ // ── Пункты навигации ── 'label' => [ + 'FAQ & Ghid' => 'FAQ и гид', 'API Tokens' => 'API токены', 'Achiziții' => 'Закупки', 'Angajați' => 'Сотрудники', diff --git a/resources/views/components/hint.blade.php b/resources/views/components/hint.blade.php new file mode 100644 index 0000000..82bcb5d --- /dev/null +++ b/resources/views/components/hint.blade.php @@ -0,0 +1,37 @@ +@props(['key']) +@php + $key = $key ?? null; + $hint = $key ? \App\Support\Hints::get($key) : null; + $user = auth()->user(); + $show = $hint && $user && $user->shouldSeeHint($key); + if (! $show) { return; } + $lc = \App\Support\Hints::locale(); + $title = $hint['title'][$lc] ?? $hint['title']['ro']; + $body = $hint['body'][$lc] ?? $hint['body']['ro']; + $links = $hint['links'] ?? []; + $next = $hint['next'][$lc] ?? null; +@endphp + + +
+
+ {{ $title }} + +
+
{{ $body }}
+ @if ($next) +
→ {{ $next }}
+ @endif + @foreach ($links as $ln) + + {{ $ln['label_' . $lc] ?? $ln['label_ro'] }} → + + @endforeach +
+
diff --git a/resources/views/filament/tenant/pages/faq.blade.php b/resources/views/filament/tenant/pages/faq.blade.php new file mode 100644 index 0000000..4aaabcc --- /dev/null +++ b/resources/views/filament/tenant/pages/faq.blade.php @@ -0,0 +1,83 @@ + +@php + $lc = \App\Support\Hints::locale(); + $areas = $this->getAreas(); + $grouped = $this->getGrouped(); + $user = auth()->user(); +@endphp + + + +
+
+ +
+ {{ __('Sugestii pe pagini:') }} +
+ @csrf + +
+
+ @csrf + +
+
+
+ + @if (empty($grouped)) +
{{ __('Nicio potrivire pentru „:q".', ['q' => $search]) }}
+ @endif + + @foreach ($grouped as $areaKey => $hints) + @php $area = $areas[$areaKey] ?? null; @endphp +
+
+ {{ $area['icon'] ?? '📖' }} + {{ $area['label'][$lc] ?? $areaKey }} + ({{ count($hints) }}) +
+ @foreach ($hints as $key => $h) +
+ {{ $h['title'][$lc] ?? $h['title']['ro'] }} +
+ {{ $h['body'][$lc] ?? $h['body']['ro'] }} + @if (! empty($h['next'][$lc])) +
→ {{ $h['next'][$lc] }}
+ @endif + @if (! empty($h['links'])) + + @endif +
+
+ @endforeach +
+ @endforeach +
+
diff --git a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php index 48b480a..59051c8 100644 --- a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php +++ b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php @@ -379,7 +379,7 @@ {{-- ── TOP BAR ── --}}
- {{ __('Fișă') }} {{ $wo->number }} + {{ __('Fișă') }} {{ $wo->number }} {{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }} @@ -696,6 +696,7 @@ + ⬇️ {{ __('Descarcă factură (PDF)') }} @@ -809,7 +810,7 @@ @endphp
-

{{ __('Chat client') }}

+

{{ __('Chat client') }}

@if ($telegramEnabled && $wo->client?->telegram_chat_id) ✓ Telegram @elseif ($whatsappEnabled && $wo->client?->phone) diff --git a/routes/web.php b/routes/web.php index 8bffa23..78e806d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,6 +47,15 @@ Route::post('/payments/paypal/webhook', [\App\Http\Controllers\PaymentController Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook']) ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); +// Hint system — dismiss / toggle / reset (auth required, tenant-scoped). +Route::middleware('auth:web')->group(function () { + Route::post('/app/hints/{key}/dismiss', [\App\Http\Controllers\HintController::class, 'dismiss']) + ->where('key', '[a-z0-9._-]+') + ->name('hints.dismiss'); + Route::post('/app/hints/toggle', [\App\Http\Controllers\HintController::class, 'toggle'])->name('hints.toggle'); + Route::post('/app/hints/reset', [\App\Http\Controllers\HintController::class, 'reset'])->name('hints.reset'); +}); + // PDF for appointments (used by CalendarBoard "PDF programări" button). // Inline preview by default; ?download=1 forces attachment. Route::get('/app/appointments/pdf', function (Request $request) {