feat(hints): contextual help system + FAQ page (MVP)
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.
* <x-hint key="wo.dashboard.overview" /> 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 <x-hint>
tags where useful. Filament resource fields can also reuse the same
copy via ->hint()/->helperText().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Pages;
|
||||||
|
|
||||||
|
use App\Support\Hints;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAQ / help page — renders the entire hints registry grouped by area
|
||||||
|
* with a client-side search box. Same content that surfaces contextually
|
||||||
|
* in "?" popovers around the app.
|
||||||
|
*/
|
||||||
|
class Faq extends Page
|
||||||
|
{
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-question-mark-circle';
|
||||||
|
|
||||||
|
protected string $view = 'filament.tenant.pages.faq';
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('nav.label.FAQ & Ghid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationGroup(): ?string
|
||||||
|
{
|
||||||
|
return __('nav.group.Admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return __('FAQ & Ghid utilizator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeading(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAreas(): array
|
||||||
|
{
|
||||||
|
return Hints::AREAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGrouped(): array
|
||||||
|
{
|
||||||
|
$q = mb_strtolower(trim($this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class HintController extends Controller
|
||||||
|
{
|
||||||
|
public function dismiss(Request $request, string $key)
|
||||||
|
{
|
||||||
|
$user = $request->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'company_id', 'name', 'email', 'phone', 'avatar_url',
|
'company_id', 'name', 'email', 'phone', 'avatar_url',
|
||||||
'role', 'status', 'locale',
|
'role', 'status', 'locale',
|
||||||
|
'hints_enabled', 'dismissed_hints',
|
||||||
'specialization', 'color', 'hourly_rate', 'internal_margin_pct',
|
'specialization', 'color', 'hourly_rate', 'internal_margin_pct',
|
||||||
'email_verified_at', 'password', 'last_login_at',
|
'email_verified_at', 'password', 'last_login_at',
|
||||||
'email_authentication_at',
|
'email_authentication_at',
|
||||||
@@ -53,9 +54,18 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
'app_authentication_secret' => 'encrypted',
|
'app_authentication_secret' => 'encrypted',
|
||||||
'app_authentication_recovery_codes' => 'encrypted:array',
|
'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
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
return $panel->getId() === 'tenant'
|
return $panel->getId() === 'tenant'
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized registry of in-app hints and the FAQ they generate.
|
||||||
|
*
|
||||||
|
* Each hint has:
|
||||||
|
* key — stable identifier used for dismiss + Blade component reference
|
||||||
|
* area — grouping bucket ("service", "crm", "depozit", "finante")
|
||||||
|
* title — short title (all 3 languages)
|
||||||
|
* body — 1-3 sentence explanation (all 3 languages)
|
||||||
|
* links — optional related pages (label + url in current locale)
|
||||||
|
* next — optional "what to do next" label (current locale)
|
||||||
|
*
|
||||||
|
* The FAQ page renders every entry grouped by area with a search box.
|
||||||
|
* The <x-hint> 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<string, array{area:string,title:array,body:array,links?:array,next?: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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $t) {
|
||||||
|
$t->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
":n poziții importate în Purchase nouă": ":n items imported into a new Purchase",
|
":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.",
|
"> 0 calculează automat prețul client.": "> 0 auto-calculates client price.",
|
||||||
"A1-03": "A1-03",
|
"A1-03": "A1-03",
|
||||||
|
"ACTIVE": "ON",
|
||||||
"AGRNMD2X": "AGRNMD2X",
|
"AGRNMD2X": "AGRNMD2X",
|
||||||
"AI provider": "AI provider",
|
"AI provider": "AI provider",
|
||||||
"AI: preț recomandat": "AI: recommended price",
|
"AI: preț recomandat": "AI: recommended price",
|
||||||
@@ -372,6 +373,7 @@
|
|||||||
"Category": "Category",
|
"Category": "Category",
|
||||||
"Caută": "Search",
|
"Caută": "Search",
|
||||||
"Caută client, mașină, număr...": "Search client, vehicle, number...",
|
"Caută client, mașină, număr...": "Search client, vehicle, number...",
|
||||||
|
"Caută în ghid...": "Search the guide...",
|
||||||
"Caută în tabel": "Search in table",
|
"Caută în tabel": "Search in table",
|
||||||
"Cauza adresării": "Reason for request",
|
"Cauza adresării": "Reason for request",
|
||||||
"Caz asigurare": "Insurance case",
|
"Caz asigurare": "Insurance case",
|
||||||
@@ -825,6 +827,7 @@
|
|||||||
"Eșapament": "Exhaust",
|
"Eșapament": "Exhaust",
|
||||||
"Ești sigur?": "Are you sure?",
|
"Ești sigur?": "Are you sure?",
|
||||||
"Eșuat": "Failed",
|
"Eșuat": "Failed",
|
||||||
|
"FAQ & Ghid utilizator": "FAQ & User guide",
|
||||||
"FIȘĂ DE LUCRU": "WORK ORDER",
|
"FIȘĂ DE LUCRU": "WORK ORDER",
|
||||||
"Facturi": "Invoices",
|
"Facturi": "Invoices",
|
||||||
"Facturi & abonament": "Facturi & abonament",
|
"Facturi & abonament": "Facturi & abonament",
|
||||||
@@ -952,6 +955,7 @@
|
|||||||
"IBAN:": "IBAN:",
|
"IBAN:": "IBAN:",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"IDNO / CUI": "IDNO / CUI",
|
"IDNO / CUI": "IDNO / CUI",
|
||||||
|
"INACTIVE": "OFF",
|
||||||
"INJECTOARE": "INJECTORS",
|
"INJECTOARE": "INJECTORS",
|
||||||
"ITP": "MOT",
|
"ITP": "MOT",
|
||||||
"Ianuarie": "January",
|
"Ianuarie": "January",
|
||||||
@@ -1281,6 +1285,7 @@
|
|||||||
"Nicio piesă montată.": "No parts installed.",
|
"Nicio piesă montată.": "No parts installed.",
|
||||||
"Nicio plată": "No payments",
|
"Nicio plată": "No payments",
|
||||||
"Nicio plată în perioada selectată.": "No payments in selected period.",
|
"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 această perioadă.": "No appointments in this period.",
|
||||||
"Nicio programare în perioada selectată.": "No appointments in selected period.",
|
"Nicio programare în perioada selectată.": "No appointments in selected period.",
|
||||||
"Niciodată": "Never",
|
"Niciodată": "Never",
|
||||||
@@ -1350,6 +1355,7 @@
|
|||||||
"Nu există date": "No data",
|
"Nu există date": "No data",
|
||||||
"Nu există rezultate": "No results",
|
"Nu există rezultate": "No results",
|
||||||
"Nu găsit": "Not found",
|
"Nu găsit": "Not found",
|
||||||
|
"Nu mai arăta": "Don't show again",
|
||||||
"Nu pot porni camera: ": "Cannot start camera: ",
|
"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 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!",
|
"Nu te poți șterge pe tine!": "You can't delete yourself!",
|
||||||
@@ -1938,6 +1944,7 @@
|
|||||||
"Subtotal piese:": "Subtotal piese:",
|
"Subtotal piese:": "Subtotal piese:",
|
||||||
"Succes": "Success",
|
"Succes": "Success",
|
||||||
"Sugerează revizie / piese de uzură.": "Suggest service / wear parts.",
|
"Sugerează revizie / piese de uzură.": "Suggest service / wear parts.",
|
||||||
|
"Sugestii pe pagini:": "In-page hints:",
|
||||||
"Sumă": "Amount",
|
"Sumă": "Amount",
|
||||||
"Sumă achitată": "Sumă achitată",
|
"Sumă achitată": "Sumă achitată",
|
||||||
"Sumă:": "Sumă:",
|
"Sumă:": "Sumă:",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'label' => [
|
'label' => [
|
||||||
|
'FAQ & Ghid' => 'FAQ & Guide',
|
||||||
'API Tokens' => 'API Tokens',
|
'API Tokens' => 'API Tokens',
|
||||||
'Achiziții' => 'Purchases',
|
'Achiziții' => 'Purchases',
|
||||||
'Angajați' => 'Employees',
|
'Angajați' => 'Employees',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ return [
|
|||||||
|
|
||||||
// ── Individual navigation labels ──
|
// ── Individual navigation labels ──
|
||||||
'label' => [
|
'label' => [
|
||||||
|
'FAQ & Ghid' => 'FAQ & Ghid',
|
||||||
'API Tokens' => 'API Tokens',
|
'API Tokens' => 'API Tokens',
|
||||||
'Achiziții' => 'Achiziții',
|
'Achiziții' => 'Achiziții',
|
||||||
'Angajați' => 'Angajați',
|
'Angajați' => 'Angajați',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
":n poziții importate în Purchase nouă": ":n позиций импортировано в новый заказ",
|
":n poziții importate în Purchase nouă": ":n позиций импортировано в новый заказ",
|
||||||
"> 0 calculează automat prețul client.": "> 0 автоматически рассчитывает цену клиента.",
|
"> 0 calculează automat prețul client.": "> 0 автоматически рассчитывает цену клиента.",
|
||||||
"A1-03": "A1-03",
|
"A1-03": "A1-03",
|
||||||
|
"ACTIVE": "ВКЛ",
|
||||||
"AGRNMD2X": "AGRNMD2X",
|
"AGRNMD2X": "AGRNMD2X",
|
||||||
"AI provider": "AI-провайдер",
|
"AI provider": "AI-провайдер",
|
||||||
"AI: preț recomandat": "AI: рекомендованная цена",
|
"AI: preț recomandat": "AI: рекомендованная цена",
|
||||||
@@ -372,6 +373,7 @@
|
|||||||
"Category": "Категория",
|
"Category": "Категория",
|
||||||
"Caută": "Найти",
|
"Caută": "Найти",
|
||||||
"Caută client, mașină, număr...": "Поиск клиента, авто, номера...",
|
"Caută client, mașină, număr...": "Поиск клиента, авто, номера...",
|
||||||
|
"Caută în ghid...": "Поиск по гиду...",
|
||||||
"Caută în tabel": "Поиск в таблице",
|
"Caută în tabel": "Поиск в таблице",
|
||||||
"Cauza adresării": "Причина обращения",
|
"Cauza adresării": "Причина обращения",
|
||||||
"Caz asigurare": "Страховой случай",
|
"Caz asigurare": "Страховой случай",
|
||||||
@@ -825,6 +827,7 @@
|
|||||||
"Eșapament": "Выхлоп",
|
"Eșapament": "Выхлоп",
|
||||||
"Ești sigur?": "Ты уверен?",
|
"Ești sigur?": "Ты уверен?",
|
||||||
"Eșuat": "Ошибка",
|
"Eșuat": "Ошибка",
|
||||||
|
"FAQ & Ghid utilizator": "FAQ и гид пользователя",
|
||||||
"FIȘĂ DE LUCRU": "ЗАКАЗ-НАРЯД",
|
"FIȘĂ DE LUCRU": "ЗАКАЗ-НАРЯД",
|
||||||
"Facturi": "Счета",
|
"Facturi": "Счета",
|
||||||
"Facturi & abonament": "Facturi & abonament",
|
"Facturi & abonament": "Facturi & abonament",
|
||||||
@@ -952,6 +955,7 @@
|
|||||||
"IBAN:": "IBAN:",
|
"IBAN:": "IBAN:",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"IDNO / CUI": "IDNO / CUI",
|
"IDNO / CUI": "IDNO / CUI",
|
||||||
|
"INACTIVE": "ВЫКЛ",
|
||||||
"INJECTOARE": "ФОРСУНКИ",
|
"INJECTOARE": "ФОРСУНКИ",
|
||||||
"ITP": "ТО",
|
"ITP": "ТО",
|
||||||
"Ianuarie": "Январь",
|
"Ianuarie": "Январь",
|
||||||
@@ -1281,6 +1285,7 @@
|
|||||||
"Nicio piesă montată.": "Запчасти не установлены.",
|
"Nicio piesă montată.": "Запчасти не установлены.",
|
||||||
"Nicio plată": "Нет платежей",
|
"Nicio plată": "Нет платежей",
|
||||||
"Nicio plată în perioada selectată.": "Нет платежей за выбранный период.",
|
"Nicio plată în perioada selectată.": "Нет платежей за выбранный период.",
|
||||||
|
"Nicio potrivire pentru „:q\".": "Нет совпадений для «:q».",
|
||||||
"Nicio programare în această perioadă.": "Нет записей в этом периоде.",
|
"Nicio programare în această perioadă.": "Нет записей в этом периоде.",
|
||||||
"Nicio programare în perioada selectată.": "Нет записей за выбранный период.",
|
"Nicio programare în perioada selectată.": "Нет записей за выбранный период.",
|
||||||
"Niciodată": "Никогда",
|
"Niciodată": "Никогда",
|
||||||
@@ -1350,6 +1355,7 @@
|
|||||||
"Nu există date": "Нет данных",
|
"Nu există date": "Нет данных",
|
||||||
"Nu există rezultate": "Нет результатов",
|
"Nu există rezultate": "Нет результатов",
|
||||||
"Nu găsit": "Не найдено",
|
"Nu găsit": "Не найдено",
|
||||||
|
"Nu mai arăta": "Больше не показывать",
|
||||||
"Nu pot porni camera: ": "Не удалось запустить камеру: ",
|
"Nu pot porni camera: ": "Не удалось запустить камеру: ",
|
||||||
"Nu re-trimite mai des de X zile": "Не отправлять повторно чаще чем раз в X дней",
|
"Nu re-trimite mai des de X zile": "Не отправлять повторно чаще чем раз в X дней",
|
||||||
"Nu te poți șterge pe tine!": "Нельзя удалить себя!",
|
"Nu te poți șterge pe tine!": "Нельзя удалить себя!",
|
||||||
@@ -1938,6 +1944,7 @@
|
|||||||
"Subtotal piese:": "Subtotal piese:",
|
"Subtotal piese:": "Subtotal piese:",
|
||||||
"Succes": "Успех",
|
"Succes": "Успех",
|
||||||
"Sugerează revizie / piese de uzură.": "Предложите ТО / расходники.",
|
"Sugerează revizie / piese de uzură.": "Предложите ТО / расходники.",
|
||||||
|
"Sugestii pe pagini:": "Подсказки на страницах:",
|
||||||
"Sumă": "Сумма",
|
"Sumă": "Сумма",
|
||||||
"Sumă achitată": "Sumă achitată",
|
"Sumă achitată": "Sumă achitată",
|
||||||
"Sumă:": "Sumă:",
|
"Sumă:": "Sumă:",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ return [
|
|||||||
|
|
||||||
// ── Пункты навигации ──
|
// ── Пункты навигации ──
|
||||||
'label' => [
|
'label' => [
|
||||||
|
'FAQ & Ghid' => 'FAQ и гид',
|
||||||
'API Tokens' => 'API токены',
|
'API Tokens' => 'API токены',
|
||||||
'Achiziții' => 'Закупки',
|
'Achiziții' => 'Закупки',
|
||||||
'Angajați' => 'Сотрудники',
|
'Angajați' => 'Сотрудники',
|
||||||
|
|||||||
@@ -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
|
||||||
|
<span x-data="{ open: false }" style="display:inline-block;position:relative;">
|
||||||
|
<button type="button" @click.prevent="open = !open" @click.outside="open = false"
|
||||||
|
title="{{ $title }}"
|
||||||
|
style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:#3b82f6;color:#fff;font-size:11px;font-weight:700;border:none;cursor:pointer;line-height:1;vertical-align:middle;margin-left:4px;flex-shrink:0;">?</button>
|
||||||
|
<div x-show="open" x-cloak
|
||||||
|
@keydown.escape.window="open = false"
|
||||||
|
style="position:absolute;top:24px;left:0;z-index:1000;width:320px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 10px 25px rgba(0,0,0,0.12);padding:12px 14px;text-align:left;color:#111;font-size:13px;line-height:1.4;">
|
||||||
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:6px;">
|
||||||
|
<b style="font-size:13px;">{{ $title }}</b>
|
||||||
|
<button type="button"
|
||||||
|
onclick="fetch('{{ route('hints.dismiss', ['key' => $key]) }}', {method:'POST', headers:{'X-CSRF-TOKEN':'{{ csrf_token() }}','Accept':'application/json'}}).then(()=>this.closest('span').remove());"
|
||||||
|
style="background:none;border:none;color:#9ca3af;cursor:pointer;font-size:16px;padding:0;line-height:1;" title="{{ __('Nu mai arăta') }}">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="color:#374151;margin-bottom:8px;">{{ $body }}</div>
|
||||||
|
@if ($next)
|
||||||
|
<div style="color:#059669;font-size:12px;margin-bottom:8px;">→ {{ $next }}</div>
|
||||||
|
@endif
|
||||||
|
@foreach ($links as $ln)
|
||||||
|
<a href="{{ url($ln['url']) }}" style="display:inline-block;font-size:12px;color:#2563eb;text-decoration:none;margin-right:8px;">
|
||||||
|
{{ $ln['label_' . $lc] ?? $ln['label_ro'] }} →
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
@php
|
||||||
|
$lc = \App\Support\Hints::locale();
|
||||||
|
$areas = $this->getAreas();
|
||||||
|
$grouped = $this->getGrouped();
|
||||||
|
$user = auth()->user();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.faq-shell { max-width: 960px; margin: 0 auto; }
|
||||||
|
.faq-top { display: flex; gap: 12px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
|
||||||
|
.faq-search { flex: 1; min-width: 260px; padding: 10px 14px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 14px; }
|
||||||
|
.faq-toggle { display: flex; gap: 8px; align-items: center; font-size: 13px; }
|
||||||
|
.faq-section { margin-bottom: 32px; }
|
||||||
|
.faq-section-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 15px; font-weight: 600; color: #111; }
|
||||||
|
.faq-section-hd .faq-icon { font-size: 20px; }
|
||||||
|
.faq-item { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
|
||||||
|
.faq-item summary { padding: 12px 16px; cursor: pointer; font-weight: 500; font-size: 14px; list-style: none; display: flex; justify-content: space-between; align-items: center; background: #fafafa; user-select: none; }
|
||||||
|
.faq-item summary::-webkit-details-marker { display: none; }
|
||||||
|
.faq-item summary::after { content: '▾'; color: #9ca3af; font-size: 12px; }
|
||||||
|
.faq-item[open] summary { background: #eff6ff; }
|
||||||
|
.faq-item[open] summary::after { content: '▴'; }
|
||||||
|
.faq-body { padding: 14px 16px; font-size: 13px; line-height: 1.5; color: #374151; }
|
||||||
|
.faq-next { color: #059669; font-size: 12px; margin-top: 8px; }
|
||||||
|
.faq-links { margin-top: 10px; display: flex; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.faq-links a { font-size: 12px; color: #2563eb; text-decoration: none; }
|
||||||
|
.faq-empty { color: #9ca3af; text-align: center; padding: 40px 20px; font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="faq-shell">
|
||||||
|
<div class="faq-top">
|
||||||
|
<input type="text" class="faq-search" placeholder="{{ __('Caută în ghid...') }}" wire:model.live.debounce.300ms="search">
|
||||||
|
<div class="faq-toggle">
|
||||||
|
<span>{{ __('Sugestii pe pagini:') }}</span>
|
||||||
|
<form method="POST" action="{{ route('hints.toggle') }}" style="display:inline;">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" style="padding:6px 12px;border-radius:6px;border:1px solid #d1d5db;background:{{ $user?->hints_enabled ? '#10b981' : '#f3f4f6' }};color:{{ $user?->hints_enabled ? '#fff' : '#374151' }};cursor:pointer;font-size:12px;font-weight:500;">
|
||||||
|
{{ $user?->hints_enabled ? __('ACTIVE') : __('INACTIVE') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="{{ route('hints.reset') }}" style="display:inline;">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" style="padding:6px 12px;border-radius:6px;border:1px solid #d1d5db;background:#fff;color:#374151;cursor:pointer;font-size:12px;">
|
||||||
|
{{ __('Resetează') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (empty($grouped))
|
||||||
|
<div class="faq-empty">{{ __('Nicio potrivire pentru „:q".', ['q' => $search]) }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@foreach ($grouped as $areaKey => $hints)
|
||||||
|
@php $area = $areas[$areaKey] ?? null; @endphp
|
||||||
|
<div class="faq-section">
|
||||||
|
<div class="faq-section-hd">
|
||||||
|
<span class="faq-icon">{{ $area['icon'] ?? '📖' }}</span>
|
||||||
|
<span>{{ $area['label'][$lc] ?? $areaKey }}</span>
|
||||||
|
<span style="color:#9ca3af;font-weight:400;font-size:12px;">({{ count($hints) }})</span>
|
||||||
|
</div>
|
||||||
|
@foreach ($hints as $key => $h)
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary>{{ $h['title'][$lc] ?? $h['title']['ro'] }}</summary>
|
||||||
|
<div class="faq-body">
|
||||||
|
{{ $h['body'][$lc] ?? $h['body']['ro'] }}
|
||||||
|
@if (! empty($h['next'][$lc]))
|
||||||
|
<div class="faq-next">→ {{ $h['next'][$lc] }}</div>
|
||||||
|
@endif
|
||||||
|
@if (! empty($h['links']))
|
||||||
|
<div class="faq-links">
|
||||||
|
@foreach ($h['links'] as $ln)
|
||||||
|
<a href="{{ url($ln['url']) }}">{{ $ln['label_' . $lc] ?? $ln['label_ro'] }} →</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
|
|
||||||
{{-- ── TOP BAR ── --}}
|
{{-- ── TOP BAR ── --}}
|
||||||
<div class="wd-topbar">
|
<div class="wd-topbar">
|
||||||
<span class="wd-wo-title">{{ __('Fișă') }} {{ $wo->number }}</span>
|
<span class="wd-wo-title">{{ __('Fișă') }} {{ $wo->number }}<x-hint key="wo.dashboard.overview" /></span>
|
||||||
<span class="wd-status {{ $statusClass }}">
|
<span class="wd-status {{ $statusClass }}">
|
||||||
{{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }}
|
{{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }}
|
||||||
<span style="opacity:.6;">▾</span>
|
<span style="opacity:.6;">▾</span>
|
||||||
@@ -696,6 +696,7 @@
|
|||||||
<button type="button" class="wd-btn" @click="pdfOpen = true">
|
<button type="button" class="wd-btn" @click="pdfOpen = true">
|
||||||
🔍 {{ __('Vizualizează factură (PDF)') }}
|
🔍 {{ __('Vizualizează factură (PDF)') }}
|
||||||
</button>
|
</button>
|
||||||
|
<span style="display:inline-block;"><x-hint key="wo.dashboard.pdf" /></span>
|
||||||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}">
|
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}">
|
||||||
⬇️ {{ __('Descarcă factură (PDF)') }}
|
⬇️ {{ __('Descarcă factură (PDF)') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -809,7 +810,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div class="wd-card">
|
<div class="wd-card">
|
||||||
<div class="wd-card-hd">
|
<div class="wd-card-hd">
|
||||||
<h3>{{ __('Chat client') }}</h3>
|
<h3>{{ __('Chat client') }}<x-hint key="wo.dashboard.chat" /></h3>
|
||||||
@if ($telegramEnabled && $wo->client?->telegram_chat_id)
|
@if ($telegramEnabled && $wo->client?->telegram_chat_id)
|
||||||
<span style="font-size:10px;color:var(--wd-blue);">✓ Telegram</span>
|
<span style="font-size:10px;color:var(--wd-blue);">✓ Telegram</span>
|
||||||
@elseif ($whatsappEnabled && $wo->client?->phone)
|
@elseif ($whatsappEnabled && $wo->client?->phone)
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ Route::post('/payments/paypal/webhook', [\App\Http\Controllers\PaymentController
|
|||||||
Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook'])
|
Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook'])
|
||||||
->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
|
->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).
|
// PDF for appointments (used by CalendarBoard "PDF programări" button).
|
||||||
// Inline preview by default; ?download=1 forces attachment.
|
// Inline preview by default; ?download=1 forces attachment.
|
||||||
Route::get('/app/appointments/pdf', function (Request $request) {
|
Route::get('/app/appointments/pdf', function (Request $request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user