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>
Filament's static ::getUrl(['record' => id]) is the canonical way to
build a page URL with parameters, and it uses the actual route resolver
(handling tenant panel prefix, etc). This is cleaner than hand-building
the path and avoids any \$this-binding ambiguity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under some Livewire lifecycles \$this->record isn't hydrated when
getHeaderActions() runs, so \$this->record->id was null and the URL
came out as /app/work-orders//dashboard. Fall back to the URL segment
(always present because we're at /app/work-orders/{id}/edit) and log
which path was taken to help diagnose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The action ->url() closure was resolving \$this to the Action instance
and its ->getRecord() returned null in header-action context, so the URL
came out as /app/work-orders//dashboard.
getHeaderActions() runs after mount(), so \$this->record is guaranteed
populated. Compute the URL string once and pass it as a plain value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inside a Filament Action's ->url() closure, \$this can bind to the
Action instance rather than the Livewire page — so \$this->record was
null and the generated URL became /app/work-orders//dashboard, which
routed as record='' and hit our friendly redirect for id 0.
Switch to \$livewire and prefer getRecord() which is always populated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Livewire's Redirector doesn't have ->send(); the correct API inside a
Livewire mount() is $this->redirect($url).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instead of a bare 404 when a work-order id doesn't resolve, redirect to
the list with a persistent notification explaining why: either the WO
doesn't exist, or it belongs to a different tenant.
This is the most common 404 cause: user types /app/work-orders/2/dashboard
but WO id 2 is in another tenant's slice — the tenant scope filters it
out and find() returns null.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Made record property nullable (?WorkOrder) and mount() calls find() +
explicit abort(404) instead of findOrFail() (which pass-through the
model's tenant-scoped scope but doesn't clarify the 404 source).
Added Feature test that boots the dashboard route via tenant subdomain
(psauto.service.mir.md-style host), authenticates a user, and verifies
the page returns non-404, non-500. Test passes both with and without
route:cache, so the routing itself is fine — user-side 404 is most
likely browser cache or CDN cache.
307/307 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filament auto-generates route names from the URL slug, so my custom
Page with slug 'work-orders/{record}/dashboard' got the ugly name
'filament.tenant.pages.work-orders.{record}.dashboard' — where
'{record}' is treated as a literal segment in the name, breaking
route() lookups.
Switch all links + the header-action URL to url('/app/work-orders/'
. \$id . '/dashboard') so they resolve correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right column now shows:
- Timeline card: synthesised events from WO lifecycle fields (created,
opened, approved, closed) + payments + activity_log entries (spatie/
activitylog). Sorted newest first, scrollable.
- Chat client card: outbound notifications history (ClientNotificationLog)
+ inline send form. Prefers Telegram if client has telegram_chat_id,
else falls back to SMS/WhatsApp. Logged either way for UI history.
New bottom action bar (fixed, above footer):
- Previous / Next WO links (adjacent by id) with number preview
- Repeat order: pre-fills create form with same client/vehicle
- Close order (danger button): sets status='done' + closed_at=now, with
wire:confirm guard. Hidden when already closed.
+14 translations. Fixed ready_at reference (not in schema, removed).
All 306 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Custom Filament Page at /app/work-orders/{id}/dashboard renders a
Mitchell1-inspired 3-column layout:
- Top bar: WO#, status badge, actions (Edit / Tracking link)
- Meta header row: creation / opened / ETA / responsible / urgency /
paid amount (6 cells)
- Left sidebar (300px): Client card (avatar, phone, email, status
tag, 3-stat grid: visits/total/debt) + Vehicle card (photo, plate,
VIN, mileage, engine, gearbox) + Repair history (latest 5 for this
vehicle, links to dashboards)
- Middle: tab bar (Lucrări/Piese/Diagnostic/Foto/Documente/Note)
with Alpine-driven switching. Works & Parts show the tables read-
only; add/edit still goes through existing EditWorkOrder Filament
resource. Photos tab shows gallery from spatie/media-library.
- Right (300px): Finance summary card (works cost, parts cost,
discount, total, paid, balance) + placeholder for Timeline+Chat
(Phase 2)
- Responsive: right column collapses <1280px, left <900px.
'Vizualizare dashboard' button added on top of the existing
EditWorkOrder page so users can switch between edit form and info-
dense dashboard.
Fixed getSlug() signature (must match parent with ?Panel $panel).
+14 translations. All 306 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'PS' was baked in — wrong for multi-tenant. Now:
1. Honor $tenant->settings['protocol_prefix'] if set
2. Else use uppercase first 2 chars of tenant slug
('psauto' → 'PS', 'autoplus' → 'AP', 'plusrepair' → 'PL')
3. Fallback 'PR' if no tenant resolvable
Format changed from 'PS-{companyId}-{year}-{seq}' to '{PREFIX}-{year}-{seq}'
— dropped the redundant companyId since prefix already identifies the
tenant contextually.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior commit only landed the RO entry; the RU + EN Edits were silently
skipped (file-not-read guard). Now all 3 langs have the label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous sed insert-after-'Programări' silently failed because that key
didn't exist. Now placed alphabetically after 'Procentaj' in all 3
lang/nav.php files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior version used Cyrillic strings as __() keys — RO users saw
Cyrillic text because fallback = key. Now all PDF labels use RO as
the source-language key (e.g. 'Automobil', 'Nr. înmatriculare',
'Nr. protocol:', 'INJECTOARE'). ru.json + en.json provide translations.
+24 keys covering title, all field labels, table headers, measurement
column headers, before/after subheadings, footer note, conclusion
section, signature block, unit suffixes (km, ml).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MariaDB default collation is case-insensitive on VARCHAR unique indexes,
so 'L' and 'l' clash on units_company_id_code_unique. When a tenant had
'L' in parts.unit (uppercase) and the migration seeds 'l' (lowercase),
the extras loop tried to re-insert 'L' and errored out — blocking all
subsequent migrations (including the new injector_protocols tables).
Fix: lowercase the comparison + switch the extras insert to
insertOrIgnore so a residual race is a no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WorksRelationManager Name column: getStateUsing(labor?->label()) so
the row shows the labor's localized name (not the raw snapshot);
fallback to stored snapshot when labor is missing.
- 4 explicit empty states with helper description + icon on Works,
Parts, SubcontractJobs, Payments — replaces Filament's auto-generated
'Не найдено X / Создать X для старта.'
- 4 CreateAction titles / modal headings for all four relation managers.
- Notification 'Piesa returnată în stoc' / 'Nimic de restituit' wrapped.
- +11 translation keys.
All 306 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Migration: add nullable `labors.name_en` alongside existing name_ro/
name_ru.
- Labor model: label(?locale) accessor returns name_{locale} with
name_ro fallback.
- LaborResource form: expose 3rd 'Nume (EN)' field.
- Table + Select displays now go through label() so options show the
locale-appropriate name:
/app/labors table column,
WorkOrderResource works Select ('[category] name (Nh)'),
ServiceTemplate items Select,
ServiceComposer WorkOrderWork snapshot on create.
- Snapshot name saved on wo_works.name at creation reflects the
current locale; existing rows keep their stored snapshot untouched.
All 306 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Labor Select options now build '[__(category)] name_ro (hours __(h))'
so category chip and hour suffix translate on RU/EN. Labor name stays
as name_ro (user data — separate long-term concern).
- CreateAction gets explicit label + modalHeading (was 'Create Work
Order Work' auto-generated).
- Notification 'Piese implicite adăugate (N)' rewritten with __(':n')
placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MechanicKpi getPeriodLabel() was hardcoded to ->locale('ro') which
forced Romanian month names ('iulie 2026') regardless of the app
locale. Same on PDF appointments day-label. Both now switch to
app()->getLocale() so RU shows «июль 2026», EN 'July 2026'.
SetLocale middleware already calls Carbon::setLocale on every
request, so translatedFormat() elsewhere already respects the app
locale — no other files needed patching.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior 'Everything up-to-date' commit was a no-op because Edit failed.
Now the change lands. Splits specialization by '/' and __()-translates
each token, matching Reports.php + CalendarBoard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both the row-meta on the master matrix rows and the legend list at
the bottom split specialization by '/' and translate each token via
__(), matching the pattern already used on Reports.php.
'Suspensie / Frâne' → «Подвеска / Тормоза», 'Motor / Cutie viteze' →
«Двигатель / КПП» etc. (all tokens already in the dict).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sections Mesaj client / Sursă & Atribuire / Marketing (UTM), the full
empty-state description, plus previously-missing Lead::SOURCES enum
values (Manual, Apel, Site, Google Maps, SEO, Recomandare) and
STATUSES (Contactat, Convertit, Pierdut).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OnlineOrder::DELIVERY values (Ridicare din service / Curier / Poștă)
weren't translated; now go through I18n::opts and have RU/EN entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Currency Select ->options() had 4 raw entries (MDL/EUR/USD/UAH)
and 2 wrapped (RON/RUB); consolidated all 6 under I18n::opts()
- Push notification title 'Trimis pe N dispozitiv(e)' rebuilt with
__(':n')-placeholder; empty state + helper now go through __()
- Added 4 long helper description keys with the closing „…"
Romanian quotation marks (the previous keys were missing the
trailing quote and dot; now match Settings.php byte-for-byte)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full coverage across Logo & favicon / AI providers / Telegram bot /
Notifications (client reminders) / Shop / Currencies / Internal
margin. Includes long helper paragraphs and 'Setări salvate' /
'Tenant not resolved' notification titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filament default rendered 'Не найдено user permission overrides /
Создать user permission override для старта.' from the model class
name. Overridden with translated heading + explanatory description
+ shield-exclamation icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior 'fix' commit was a no-op because git diff was empty. This time
the Edit lands the change on disk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add Permissions::permissionLabels() mapping every slug to a
human-readable RO label (e.g. 'clients.view_all' → 'Vezi toți
clienții').
- RoleResource permission checkboxes now show translated labels
as titles + technical slug as description (so admins see both
the meaning and the key).
- PermissionOverridesRelationManager Select 'permission_id' now
shows 'Human label (technical.slug)' format, plus helper text
explaining GRANT/DENY. Same format on the display column.
- +57 translations (50 permission labels in RU/EN + role page
supporting keys + module labels).
All 306 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 'DENY — interzice dreptul' select option was missing __() wrap
- CreateAction/EditAction now have explicit label/modalHeading so
the modal shows 'Добавить исключение прав' instead of
'Создать User Permission Override' (auto-generated from class name)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status column showed raw DB enum values (active/inactive/blocked) with
no formatStateUsing → readers saw 'active' instead of the translated
'Активный / Активно' visible in the Select. Added formatStateUsing with
a match() that returns the appropriate __() key.
Locale header re-translated to 'Язык' (RU) / 'Language' (EN) since
'Локаль' is unusual for end users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Both role selects (primary + additional roles multi) passed the raw
roleLabels() array without translation; wrap with I18n::opts() so
Proprietar/Administrator/… now render translated in RU/EN.
- 2FA status text ('✓ Activat (TOTP)' / '✗ Dezactivat') split so
the checkmark stays and label runs through __().
- +3 translations (Activat (TOTP), Dezactivat, Locale) — Locale is
Filament's auto-generated headline from the `locale` field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers Identitate/Acces/2FA sections, all helper texts on user
edit page (2FA reset, role sync, additional roles, margin helper,
salary formula), placeholder examples, RBAC role labels (Proprietar,
Vizitator, Recepție), 'Română' language option, and Permission
Overrides RelationManager helpers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes 'Posturi De Lucru' title (was RO fallback) and translates the
create form placeholder + default-mechanic helper text + no-default
option label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- +6 subcontract-jobs translations (Lucrare, Termene, Returnat, ETA,
helper 'Setat manual dacă markup = 0.', empty-state description)
- Add lang/vendor/filament-forms/{ru,en}/components.php with select
no_options_message + max_items_message. Filament ships RO copy with
no RU override, so 'Nu sunt disponibile opțiuni.' bled through into
RU/EN locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fills in the untranslated Subcontractor::SPECIALTIES options (Turbo,
Variator, Casetă direcție, PDR, Electronică, Injectoare) plus
Specialitate label, empty-state heading and full description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit added translations but the source Edit failed silently
via file-not-read guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the free-text token/requests counter suffix and adds the last 6
identity-fallback keys used on the page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>