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>
Replace link-to-new-tab with Alpine.js lightbox overlay. Clicking a
thumbnail opens a full-screen dark overlay with the full-size image,
prev/next arrows (keyboard ←/→ supported), counter, and close button
(ESC or ✕ or click outside). No external libraries.
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>
FrankenPHP/Caddy does not reliably follow the public/storage symlink
when serving static files, causing 404 on all media URLs (/storage/…).
Add CADDY_SERVER_EXTRA_DIRECTIVES with an explicit handle_path block
that maps /storage/* directly to /app/storage/app/public, avoiding the
symlink entirely.
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>
- saving: always shift >= new position by +1 (no conflict check needed)
- saved: renormalize all records to 1, 2, 3... via direct DB update
(no model events triggered) to close any gaps left by the move
- Migration updated to use step 1 instead of 10
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration renumbers all categories per company as 10, 20, 30… to
eliminate existing duplicates. Observer now increments by 10 instead
of 1 to leave headroom between positions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a category is saved with a sort_order already occupied by another
record in the same company, all records at that position and above are
incremented by 1 before the save, inserting the new one cleanly at the
requested position.
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>
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>