Two-layer protection on MasterResource and UserResource:
1. Form validation via ->rules() with Rule::unique() scoped to company_id
(prevents DB hit in normal flow, shows inline field error)
2. handleRecordCreation/handleRecordUpdate catches
UniqueConstraintViolationException as fallback — shows a Filament
danger notification and halts without showing an error page
Also adds phone uniqueness validation (scoped per tenant).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added ->unique(ignoreRecord: true) scoped to company_id on the email
field in both MasterResource and UserResource. Previously a DB-level
UniqueConstraintViolationException crashed the page; now Filament shows
"Acest email este deja folosit de un alt utilizator." inline on the field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings page: new "Politica Cookie" section — toggle banner on/off,
editable RO/RU policy text (defaults to full 14-section template)
- Public route GET /cookie-policy → CookiePolicyController returns
locale-aware full policy page with tenant branding
- Cookie banner partial (Alpine.js, localStorage) shown on tracking and
landing pages; Accept/Reject stores consent, banner dismisses
- Translation keys added to lang/ro/portal.php and lang/ru/portal.php
- Alpine.js (CDN) added to tracking and landing pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. New form: Auto (vehicle) field first, client auto-filled via
updatedNewApptVehicleId() when a vehicle is selected; getAllVehicleOptions()
returns all vehicles as «plate — make model» for quick lookup
2. Scroll speed: JS intercepts wheel events on .cv-matrix-wrap and
applies ×0.45 multiplier for horizontal scroll (shift+wheel or
trackpad horizontal); scroll-behavior:smooth added to CSS
3. Event cards now show all 4 fields: 🔧 Lucrarea (title) +
👤 Client + 🚗 Auto · plate + 🕐 Ora; each shown only if present
4. Delete bug fixed: replaced onclick=confirm() (races with wire:click)
with wire:confirm= (Livewire native dialog — Cancel truly cancels)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous approach wrapped {{ $this->table }} in x-show which caused
Livewire/Alpine conflicts and broken toggling. New approach:
- viewMode is a Livewire #[Url] property (persisted in URL as ?view=)
- wire:click="setViewMode('list/tree')" triggers a clean Livewire render
- Only one view is rendered at a time (@if, not x-show)
- Tree: Alpine manages per-category open/close state client-side via
indexed array; Open/Close all buttons use setAll() helper
- Visual overhaul: tighter cards, chevron arrows, proper hover states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ListLabors now has a toggle button (persistat în localStorage) care
comută între:
- Listă: tabelul Filament grupat pe categorii (existent)
- Arbore: accordion cu toate categoriile ca noduri colapsabile,
fiecare cu tabelul manoperelor sale (Manoperă, Mod, Ore, Preț,
Cost, link Editează); butoane Deschide tot / Închide tot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DATE columns lose time information, causing timeline to show 00:00 for
car reception and WO closure. Migrated both columns to DATETIME and
updated: model casts (date → datetime), form pickers (DatePicker →
DateTimePicker with seconds:false), default to now() instead of today(),
and cleaned up defensive Carbon::parse() in getTimeline().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parts (wo_parts) already had discount_pct; now also added to wo_works.
Labor catalog gets a cost_price field as the hard minimum sell floor.
- WorkOrderWork: discount_pct + cost_price fields; total calculation
now includes discount: hours × price_per_hour × (1 − disc/100)
- Labor model: cost_price field (used to pre-fill work rows and validate)
- WorksRelationManager: discount_pct field with live helper text showing
effective price + margin %; validation blocks save if below buy cost
or below company min_discount_margin_pct; "Reducere globală" header
action applies % to all work rows (skips rows that would breach limits)
- PartsRelationManager: same validation on existing discount_pct field;
helper text (🟢/🟠/🔴) shows effective price + margin in real time;
"Reducere globală" header action for all parts
- Settings page: new "Reduceri & limite preț" section with
min_discount_margin_pct — 0 = no limit, >0 = hard block below that %
- LaborResource form: cost_price field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The read-only Achitat (MDL) display is unnecessary — the Total field
already shows the amount. Only show the editable Sumă achitată field
when partial is selected; rest de achitat remains always visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkOrderResource: pay_status is now live(); added _partial_amount
TextInput (visible only when pay_status=partial) that shows current
paid amount and accepts a new total — dehydrated so it does not save
directly; _achitat hidden when partial is selected to avoid confusion
- EditWorkOrder.afterSave: creates a Payment record for the difference
when pay_status=partial and entered amount > existing paidAmount
- WorkOrderDashboard: WithFileUploads + uploadPhotos / deletePhoto
methods using Spatie MediaLibrary
- Dashboard blade: Foto tab replaced with upload form + gallery with
per-photo delete buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows the financial breakdown inline in the Plată & total section:
- Total (MDL) — bold, computed
- Achitat (MDL) — green, from paidAmount()
- Rest de achitat (MDL) — red if unpaid, green if fully paid
All three fields are readOnly/dehydrated (display only). To register
payments, use the Plăți relation manager tab.
Translations added for RO/RU/EN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pay_status: add label Statut plată (was showing English auto-label)
- discount_pct: rename label to Reducere % (standard RO terminology)
- total: disabled() → readOnly() so field looks active (not grayed out)
while still preventing manual editing; $set() from discount live works
- Add ru/en translations: Statut plată, Reducere %
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PaymentsRelationManager: remove broken default() closure on amount;
use fillForm() on CreateAction instead (correct Filament API) so the
modal pre-fills with remaining balance (total - paid) but field stays
fully editable; add minValue/step for numeric UX
- WorkOrderResource: discount_pct is now live(onBlur) with afterStateUpdated
that recomputes works+parts+subcontract minus discount and sets the
total field immediately — no Save required to see the effect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PDF parts table: removed Cod/article column, widened Denumire column,
fixed colspan in tfoot from 4 to 3
- PaymentsRelationManager: amount field now defaults to (total - paidAmount)
so it pre-fills with the remaining balance; always editable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
afterSave() calls recalcTotal() so discount_pct changes are applied
to the total immediately on save, then fillForm() refreshes the
displayed total field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With collapsedGroupsByDefault() the 386 rows are fetched but not
rendered in DOM until expanded — no perf issue, all 17 group headers
now visible without needing to navigate pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration: replace 45 leaf categories (diag.scan, engine.mech…) with
17 root categories (diag, engine…); update labors.category by taking
the prefix before the first dot; delete the leaf LaborCategory rows
- ImportLaborCatalogCommand: rewritten to use root categories directly
so future imports go to the right level from the start
- LaborResource: add ->collapsible() to groups so user can collapse/
expand each category section to see only headers or only one group
- entrypoint.sh: remove one-time catalog import (was already done)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LaborResource: hide category column from data rows by default
(toggleable) so group headers span all columns cleanly; the group
header already shows the localized category name
- ImportLaborCatalogCommand: add --all flag to iterate every company,
refactor into importForCompany() helper, keep idempotent (skips
existing codes, updates only with --force)
- entrypoint.sh: run catalog:import-labors --all after each deploy so
all 386 operations + 45 categories are seeded automatically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LaborResource: add titlePrefixedWithLabel(false) so group headers show
just the localized category name (not "Categorie: Шины"); remove static
cache from closure to avoid Octane cross-request locale bleed
- WorkOrderDashboard: add changeStatus() Livewire method + Alpine.js
dropdown on the status badge so status can be changed directly from
the dashboard without going to the full edit form
- New artisan command catalog:import-labors {company} that reads
database/data/labor-catalog.json (386 operations, 45 leaf categories)
and upserts labor_categories + labors for the given tenant; supports
--force to update existing records by code
- Add "Status actualizat" translation to ru/en JSON
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defines a proper Tables\Grouping\Group with getTitleFromRecordUsing so
group headers use the locale-aware label from LaborCategory::optionsForSelect()
instead of the raw DB value with Filament's hardcoded "Category:" prefix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
formatStateUsing looks up localized category name from LaborCategory
so 'Anvelope' shows as 'Шины' (ru) / 'Tires' (en). Filter label
uses __('Categorie') to avoid the auto-derived 'Category' (English).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Section was Filament\Schemas\Components\Section not Forms\Components\Section
- Added ru/en translations for page title, form labels, nav label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration: adds name_ro, name_ru, name_en columns; copies name→name_ro;
seeds RU and EN translations for all 11 default categories
- LaborCategory: label() returns localized name, optionsForSelect() keyed
by canonical name (stored in labors.category) with localized label
- LaborCategoryResource: 3-column form with RO/RU/EN inputs; table shows
all three columns side by side
- LaborResource: category select and filter use optionsForSelect()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Creates labor_categories table (company-scoped) seeded with 11 defaults
- LaborCategory model with BelongsToTenant
- Full Filament CRUD resource with drag-to-reorder (sort_order)
- LaborResource category Select now reads from DB instead of hardcoded const
- Navigation: 'Categorii manopere' in Service group, sort 33
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getPluralModelLabel now uses nav.label.Norme-ore (already translated
to 'Catalog manopere'/'Каталог работ') so breadcrumb and page heading
match the sidebar navigation label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
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>
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>
- 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>
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>
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>