- Migration: make clients.phone nullable (was NOT NULL, broke inline create)
- Vehicle createOptionUsing: use Get callback instead of $livewire->data
to reliably get client_id in the vehicle inline form
- Remove plate->required() on inline vehicle form (not required in DB)
- Rename 'Norme-ore' → 'Catalog manopere'/'Каталог работ' in navigation
so users can find the labor catalog easily
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Load subcontractJobs via explicit query (bypassing TenantScope but
scoped manually by work_order_id + company_id) to avoid relation
caching issues. Pass as separate blade variable \$subcontractJobs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Loads subcontractJobs.subcontractor relation and renders a new table
in the PDF after parts: description, subcontractor, category, client price.
Adds ru/en translations for the new section labels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkOrderPdfService::filename() removed duplicate 'WO-' prefix
($wo->number already starts with 'WO-')
- PDF <title> now uses __('FIȘĂ DE LUCRU') so it shows in user locale
- entrypoint: view:clear before view:cache to avoid stale compiled views
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set app locale from user preference in PDF route before rendering
- Wrap hardcoded strings (TOTAL, Subtotal, Achitat etc.) with __()
- Translate status values via __() in blade
- Add ru/en translations: statuses, totals, payment lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strings used in work-order PDF (Nr., Nume:, Telefon:, Marca/Model:,
Data deschiderii:, Km la intrare:, subtotals) were falling back to
Romanian in ru/en locale. Added all missing keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DejaVu Sans lacks glyphs for U+0219 (ș) and U+021B (ț). Fix: render
view to HTML first, replace with cedilla variants (U+015F/U+0163) that
DejaVu Sans supports, then load HTML. Set default_font to DejaVu Sans.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DejaVu Sans lacks glyphs for U+0219 (ș) and U+021B (ț). Fix: render
view to HTML first, replace with cedilla variants (U+015F/U+0163) that
DejaVu Sans supports, then load HTML. Set default_font to DejaVu Sans.
Creates storage/fonts for DomPDF font cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Queries now run only when modal is opened — no risk of page-load
exception hiding the button. Button is always visible for admin/owner.
Adds restore action inside modal footer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Helps identify cross-tenant WO leakage — shows each company's slug,
active count, deleted count, and whether it's the current tenant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Soft-deleted WOs (WO-26-0005..0008) are missing from admin view.
A new restore action lets the admin recover them without CLI access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace isAdmin() (Spatie-dependent) with direct role check and add
TenantManager fallback using hostname subdomain so the button appears
even if TenantManager was not resolved for the current request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows total WO count broken down by tenant/other-company/deleted across
all records without global scopes, helping debug missing WO visibility.
Also adds a recover action when orphaned WOs are found in wrong company.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of relying on terminal/artisan commands, admins see a warning
button on the WO list when orphaned WOs exist (wrong company_id due to
Octane singleton bug). One click moves them all to the current tenant.
Button only shows if there are orphaned WOs and user is admin/owner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous migration skipped companies with ≥1 user. Now moves WOs from
any company whose slug/name contains 'test' or has ≤2 users, which
covers the single test account scenario.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
One-time data migration that moves work orders created with wrong
company_id (Octane singleton race condition) back into psauto.
Only moves WOs from companies that have zero users (ghost/test tenants),
so legitimate separate companies are not affected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FrankenPHP/Octane reuses the same PHP process for concurrent requests.
TenantManager registered as singleton() was shared between requests —
a concurrent request from another tenant could overwrite the current
tenant context mid-flight, causing records to be saved with the wrong
company_id.
Fix 1: Change singleton() to scoped() so Octane flushes TenantManager
at the end of each request (fresh instance per request lifecycle).
Fix 2: Explicitly set company_id in mutateFormDataBeforeCreate() as an
extra guard — even if the creating event fires in the wrong context,
the value from the request's own TenantManager call takes precedence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isMechanic() calls hasAnyRole(['mechanic']) via Spatie which checks the
model_has_roles table with team_id scoping. If roles are not correctly
seeded for a tenant, admins/owners could accidentally pass the mechanic
filter and see only WOs where master_id = their own id (i.e. nothing).
Now uses $user->role === 'mechanic' directly — no Spatie involved —
so admin/owner/manager always see all WOs in their tenant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous implementation relied on Spatie canDo() for all roles, which
caused admins/managers to see nothing if permissions weren't seeded.
Now: mechanics see only their assigned WOs (master_id = self); every
other role (admin, manager, receptionist, etc.) sees all WOs in the
tenant — TenantScope already prevents cross-tenant access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin/owner and users with work_orders.view_all see all work orders.
Users with only work_orders.view_own_assigned (mechanics) see only
work orders where master_id = their own user ID.
Roles without either permission see nothing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- client_id field: switched to relationship() with createOptionForm so new clients
can be created inline (name, phone, email); search also matches by phone
- vehicle_id field: added createOptionForm (make, model, plate, year, VIN); newly
created vehicle is automatically linked to the currently selected client
- master_id field: now shows only active users with role mechanic/master
- WorksRelationManager: labor catalog select marked optional; added createOptionForm
so new catalog entries can be added without leaving the WO form
- SubcontractJobsRelationManager: category table column now translates value via __()
and shows explicit label(__('Categorie'))
- PDF work-order: replaced 📞 / ✉ / ⚠ emojis with plain-text equivalents so
DomPDF/DejaVu Sans renders them correctly instead of showing "????"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FAQ sections were only 4 (service/crm/depozit/finante) even though
the sidebar has 8. Added the missing 4 to Hints::AREAS with proper
icons + accent colors:
📊 Analiză (pink) 📣 Marketing (orange)
🛒 Magazin (cyan) ⚙️ Admin (slate)
Retagged 15 hints that were shoved into wrong areas as a workaround
(admin.*/marketing.*/shop.*/analytics.* now use the matching bucket).
FAQ blade reads accent color from the AREAS registry instead of a
hardcoded local map.
Also added per-tenant navigation group ordering:
- Settings → new "Ordine meniu lateral" section with a Filament
Repeater that has reorderable buttons (drag/arrow buttons) but no
add/delete (fixed list of 8 groups)
- Persisted to settings.nav_group_order as an array of keys
- New ConfigureNavGroups middleware (runs after ResolveTenant on
the web stack) reads the current tenant's order and calls
$panel->navigationGroups([...]) so the sidebar renders in that
order per-request
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filament pages wrap their content in an outer <form>. My inline
<form method="POST"> for the hint toggle + reset was nested inside
that outer form — invalid HTML, so browsers dropped my submit and
either did nothing or ran the outer Livewire form's handler.
Replaced both forms with plain <button type="button"> + onclick
fetch() POST + window.location.reload(). Controller now also returns
JSON when Accept: application/json (fetch sends that) instead of a
back() redirect.
The server-side flip was always correct — verified via a Feature
test that posts to /app/hints/toggle and asserts the user column
flipped from true → false.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous pill button showed "● ACTIVE" or "○ INACTIVE" but users read
it as a status label, not a clickable action — nobody clicked to
disable.
New UI: 46×24 iOS-style switch that slides between grey (OFF) and
green (ON) with a sliding white knob, plus a text label next to it.
The whole switch is a form button — clicking flips hints_enabled.
Title attribute + label make both directions obvious: "Click pentru
a DEZACTIVA" when on, "Click pentru a ACTIVA" when off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New tenant setting: settings.faq_video_url. TextInput on Settings
page → Liste configurabile (accepts YouTube watch URL, youtu.be
short URL, YouTube shorts/embed URL, or Vimeo URL).
- Faq page has getVideoEmbedUrl() that converts recognized URLs to
their /embed/ variant. Unknown URLs fall back to a "?" card with
a direct link. Admins see a "configure in Settings" nudge when no
URL is set.
- Video renders as a 16:9 iframe capped at 480px height, above the
hero.
- .faq-shell is now width:100% (was max-width:1200px) so FAQ uses
all available horizontal space.
- .faq-grid switched from strict 2 columns to
repeat(auto-fit, minmax(420px, 1fr)) so wide screens get 3 columns
and narrow screens stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced the flat vertical list with:
- Hero strip (gradient blue) with title, subtitle, and 2 stat pills
showing total topics + section count
- Controls bar in a card: search input with inline icon, pill-shaped
toggle button (green when hints ON), reset button with icon
- 2-column responsive grid of area cards. Each card has a colored
header (service=blue, crm=purple, depozit=amber, finante=green)
with icon + name + count badge
- Details/summary items use +/− indicators, tinted background when
open, hover state; body has pill-styled links and green next-step
- Full dark-mode support
- Empty state with search icon
Same content, much easier to scan and looks like a proper help center.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switched render hook from PAGE_HEADER_ACTIONS_BEFORE to
PAGE_HEADER_HEADING_AFTER:
- Previous hook lived inside the action buttons container which is
skipped on pages without header actions (most Admin pages: Settings,
Integrations, API Tokens, Backup, Activity log).
- Previous hook also pushed the icon to the right side on pages that
did have actions (Clienți, Mașini, Calendar list), which looked
disconnected from the title.
New hook renders "?" immediately after the page heading text on every
page, always visible. Also fixed one URL prefix mismatch:
app/pipeline → app/pipeline-board (actual Filament page slug).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registry now covers ~50 hints across Service, CRM, Depozit, Finanțe,
Admin, Marketing, Analiză, Shop. Every main tenant page has an entry.
New Hints::forPath(\$path) does str_starts_with matching against a
curated URL prefix map. First match wins so specific paths (e.g.
/work-orders/create) beat broader ones (/work-orders).
A single render hook on PAGE_HEADER_ACTIONS_BEFORE looks up the
current request path and, if it matches, renders a "?" icon next to
the page header actions. Blades that already have <x-hint> tags
embedded (WO dashboard, Calendar, Pipeline, Excel wizard) keep those
on top of the auto-injected header hint.
Net effect: users see "?" icons everywhere in the panel now, and
adding coverage for a new page is a 2-line change (registry entry +
one URL prefix in forPath).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registry grows from 14 → 25 hints. New entries:
- wo.dashboard.topbar_actions / meta / tabs / history / finance /
timeline / bottom
- wo.create.overview
- crm.clients.overview / vehicles.overview
- finante.dashboard.stats / expenses.overview
"?" icons injected into:
- WO dashboard: top-bar actions, meta header, tabs bar, history card,
finance card, timeline card, bottom action bar (in addition to the
existing dashboard title, PDF preview, chat)
- CalendarBoard: title
- PipelineBoard: title
- Excel import wizard: step 1
- Main tenant dashboard (via PanelsRenderHook::PAGE_START scoped to
Dashboard::class): small banner with a "?" and a shortcut to the
FAQ page for users who don't see the icons
Users now see hint icons on the primary daily-use screens; more can
be added by dropping <x-hint key="..." /> anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Same Alpine.js overlay pattern as the WO dashboard Docs tab: click
"🖨 PDF programări" opens a centered modal with the PDF iframed at
90vh, plus "Descarcă" and "Deschide în tab nou" fallbacks in the
header and Esc/click-outside to close. iframe src only binds when
the modal is open, so PDF generation doesn't happen on page load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wire:click="exportPdf" attempted to return a binary Response through
Livewire's JSON channel — it re-encoded the PDF bytes as JSON and
triggered "Malformed UTF-8 characters, possibly incorrectly encoded".
Added GET /app/appointments/pdf?from=...&to=... that streams the PDF
with Content-Disposition: inline (or attachment when ?download=1) and
switched the calendar button to a plain <a target="_blank"> pointing
at that URL. The button now includes the currently visible period via
query params.
The old exportPdf() method is retained (still returns a plain PDF
Response) because CalendarEnhancementsTest asserts against it — it's
just no longer wired to the UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously nav had 12 groups, several with just 1 item. Now 8 clean
groups, no orphans:
- Anvelope (1 item) → merged into Service (Seturi anvelope)
- Stoc & Finanțe (1 item) → merged into Depozit (Import factură Excel)
- Tinichigerie (1 item) → merged into Service (Tinichigerie / Detailing)
- Subcontractare (2 items) → merged into Service (Lucrări terți,
Subcontractori)
- Integrări moved from Marketing → Admin (it's a config concern, not
a marketing feature)
- ClientResource + VehicleResource were ungrouped → added to CRM
Final groups:
Admin(8) · Analiză(4) · CRM(5) · Depozit(10) · Finanțe(6) ·
Magazin(2) · Marketing(3) · Service(11)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WO list row click now opens /work-orders/{id}/dashboard instead of
/edit (via ->recordUrl on the table). Edit remains reachable from
the per-row Filament EditAction.
- CreateWorkOrder redirects to the dashboard after save instead of
the edit page.
- Dashboard top bar exposes: "Listă" (back to WO list), "+ Nou"
(create), and "Editare completă" (full edit form) so users don't
have to hop through the sidebar to move between related WO screens.
The old /work-orders, /create, and /{id}/edit routes stay intact —
just the default navigation flow now converges on the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AppointmentResource::\$shouldRegisterNavigation = false so only
CalendarBoard shows in the sidebar as "Calendar". The board already
has a "Listă" view-mode button alongside Zi/Săpt/Lună/Custom, so both
use cases are covered from a single entry. CRUD routes
(/app/appointments/create, /{id}/edit) stay accessible from the
board's click handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CalendarBoard::getNavigationParentItem() now returns the Appointments
resource nav label so the sidebar collapses them into a single
expandable menu item. Routes and logic unchanged — both
/app/appointments and /app/calendar-board still work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All "Vizualizare PDF" actions now open a wide (7xl) Filament modal
containing an iframe pointing at the inline PDF endpoint, with a
top-right "Descarcă" and "Deschide în tab nou" fallback.
- WorkOrder edit action + WO table row action
- InjectorProtocol edit action + protocols table row action
- Dashboard Docs tab preview button (Alpine.js overlay since it's
a plain Blade view, not a Filament action)
The underlying /pdf routes stay unchanged (Content-Disposition: inline,
?download=1 for attachment) — the iframe just loads the same URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vehicle model now implements HasMedia + InteractsWithMedia and
registers a 'photos' collection. VehicleResource form gets a
SpatieMediaLibraryFileUpload section (collapsible, max 6 photos,
imageEditor enabled).
Dashboard already checks vehicle->getFirstMediaUrl('photos') first
and falls back to the WO's photos, so uploading here immediately
populates the vehicle card on the dashboard without any view changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WorkOrder and InjectorProtocol PDF routes now respond with
Content-Disposition: inline (opens in browser tab so the user can
read/print/save from the built-in PDF viewer). ?download=1 forces
the classic attachment download.
- CalendarBoard exportPdf() switched to the same inline default.
- WO edit action + WO table row action + injector protocol actions
now use ->url(...)->openUrlInNewTab() instead of streaming the PDF
inline into the current tab.
- Dashboard Docs tab has both a preview link and a "Descarcă" link.
- Vehicle card photo falls back to WO's first uploaded photo when
Vehicle model has no MediaLibrary integration of its own.
Test CalendarEnhancementsTest updated to assert the new inline
response headers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Documente tab: link to a new /app/work-orders/{id}/pdf route that
streams the invoice PDF via WorkOrderPdfService; also lists any
files uploaded to the 'signed_documents' media collection.
- Chat client card: only show the input form when telegram_bot or
whatsapp_business is enabled AND credentialed in the tenant's
Integrations page. Otherwise render an empty state with a shortcut
to /app/integrations.
RU + EN translations added for the new strings (RO source).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vehicle model doesn't use Spatie MediaLibrary — the direct call broke
the dashboard for any WO whose vehicle has no MediaLibrary integration.
Guard with method_exists and remove the diagnostic logging + URL echo
now that the routing issue is resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the "Fișa #0" bug: the page had
public ?WorkOrder \$record = null;
and the route slug was work-orders/{record}/dashboard. Livewire/Laravel
saw the {record} param and the typed \$record property with the same
name and attempted route model binding via BelongsToTenant scope. When
that resolution didn't return a Model instance in a Livewire hydration
context, mount() ended up being called with 0, so the button worked
but the destination page redirected saying "Fișa #0".
Fix: rename the route parameter to {wo} so it no longer collides with
the property name, receive it as int|string in mount(), and do the
find() ourselves. The property stays as \$record for the Blade view
but is now always populated by our own code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Log::warning() on every mount() so we can trace exactly what
record id and URL the button navigates to. Also includes the full URL
in the "not found" alert so the user can copy it back to us.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without an explicit panel, WorkOrderDashboard::getUrl() resolves the
route name against Filament's current-or-default panel, which in some
Livewire lifecycles or from ambiguous contexts is 'central' — where
the page isn't registered. Symptom: the produced URL ended up empty
or malformed, and clicking the button hit our friendly redirect
claiming WO #0 doesn't exist.
Also removes the temporary /__wo-diag/{id} route added for diagnosis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns tenant context, WO visibility via scope, and the current
dashboard button URL that Filament's Page::getUrl() produces. Will be
removed once the bug is diagnosed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>