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,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 ── --}}
|
||||
<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 }}">
|
||||
{{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }}
|
||||
<span style="opacity:.6;">▾</span>
|
||||
@@ -696,6 +696,7 @@
|
||||
<button type="button" class="wd-btn" @click="pdfOpen = true">
|
||||
🔍 {{ __('Vizualizează factură (PDF)') }}
|
||||
</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') }}">
|
||||
⬇️ {{ __('Descarcă factură (PDF)') }}
|
||||
</a>
|
||||
@@ -809,7 +810,7 @@
|
||||
@endphp
|
||||
<div class="wd-card">
|
||||
<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)
|
||||
<span style="font-size:10px;color:var(--wd-blue);">✓ Telegram</span>
|
||||
@elseif ($whatsappEnabled && $wo->client?->phone)
|
||||
|
||||
Reference in New Issue
Block a user