- 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>
Previous commit added the translation entries and commit message claimed
this, but the Edit tool call failed silently (file-not-read guard) so
the source change never landed. This time it's applied.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Convert static $modelLabel/$pluralModelLabel/$title to getter methods
- Wrap ->label()/->placeholder()/->helperText()/->description()/->title()/->body() args
- Wrap Section::make()/Fieldset::make()/Notification::make()->title() args
- Fix RelationManagers::getTitle() signature to match parent (Model, string)
- Fix Pages::getTitle() to instance method (BasePage::getTitle is non-static)
- Extend lang/ru.json + lang/en.json with 700+ common terms; identity fallback for the rest
- Remove duplicate getters in 5 resources that had manual getModelLabel already
All 306 tests pass. Missing translations fall back to the RO key so the UI never breaks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User screenshots showed the tenant admin panel (Filament) had sidebar
labels stuck in Romanian even when switching to Russian: 'Cereri',
'Calendar vizual', 'Atelierul meu', 'KPI mecanici', 'Fișe lucru',
'Norme-ore', 'Tehnicieni', 'Șabloane servicii', 'Depozite', 'Scaner',
'Depozit', 'VIN-căutare', 'Furnizori', 'Achiziții', 'Procentaj',
'Coeficienți preț', plus all group headers.
Root cause: every Filament Resource and Page had static properties
'protected static ?string $navigationLabel = "Fișe lucru"' — string
literals baked into class definitions. Static properties don't run
through the translation layer.
Fix in two parts:
1. New translation files with 52 label keys + 12 group keys:
- lang/ro/nav.php — Romanian (identity)
- lang/ru/nav.php — full Russian translations (Заказ-наряды,
Автомобили, Клиенты, Календарь, Моя мастерская, Механики KPI,
Настройки, etc.)
- lang/en/nav.php — English translations (Work orders, Vehicles,
Clients, Calendar, My workshop, Mechanic KPI, Settings, etc.)
Keyed by the Romanian original so lookups map 1:1 —
'nav.label.Fișe lucru' returns 'Заказ-наряды' in RU, 'Work orders'
in EN, 'Fișe lucru' in RO.
2. Python transformer converted 54 files:
- 33 Filament Tenant Resources
- 15 Filament Tenant Pages
- 4 Filament Central Resources
- 1 Filament Central Page
- 1 Widget
Each 'protected static ?string $navigationLabel = "X";' became
'public static function getNavigationLabel(): string { return
__("nav.label.X"); }'. Same treatment for $navigationGroup.
Cleanup: 6 resources already had manually-added getNavigationLabel
methods from an earlier partial effort — those used flat JSON keys
(__("Cereri")) that never resolved. Deduped so only the nav.label.*
version remains.
Untouched (intentional):
- $modelLabel / $pluralModelLabel (used in breadcrumbs and headings —
still hardcoded, next tier of work)
- Section titles, column headers, form field labels (medium priority)
- $navigationSort (numeric, no translation needed)
- $navigationIcon (icon reference)
Suite: 306 passed (853 assertions). Unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes:
== 1. Language switcher (RO/RU/EN) in portal headers ==
New partial resources/views/partials/lang-switcher.blade.php with two
visual styles:
- style='chip' (default) — white-tinted glass for colored headers
(shop nav bar, tracking page hero)
- style='light' — outlined buttons for pale backgrounds (invitation
accept card)
Each button is a POST form to the existing /locale/{lang} route which:
- puts locale in session
- persists to user.locale if authenticated
- redirects back to the same page
Included in:
- resources/views/shop/layout.blade.php (right side of top nav,
after login/register)
- resources/views/tracking/show.blade.php (top-right of hero header,
absolutely positioned)
- resources/views/invitations/accept.blade.php (top-right of card,
above welcome heading, light style)
Current locale button is highlighted (opaque white on colored bg /
blue on pale bg). Others are muted until hovered.
Two new translation keys:
- portal.common.language (RO: Limbă / RU: Язык)
== 2. apply_margin toggle back in "Plată & total" ==
Previous session's edit didn't persist to the file. Now confirmed in
place: WorkOrderResource form's "Plată & total" section shows a
"Aplică marjă internă" toggle between discount_pct and
override_margin_pct. Gated by FINANCE_VIEW_INTERNAL_MARGIN so only
owner / admin / manager / accountant see it. Default = true.
When toggled off on a Fișă, WorkOrderWork::saving hook writes
salary_base = total for every line on that WO (no reduction).
Backend logic already in place — this commit fixes the missing UI
control.
Suite: 303 passed (840 assertions). Unchanged — refactor + view only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three UX changes to /app/work-orders/{id}/edit per user request:
1. Manopere / Piese / Subcontract / Plăți now appear as top-level tabs
2. Header info (Antet, Diagnostic, Foto, Tracking, Plată) is compact
and collapsed by default
3. Left navigation column can be narrowed via Filament's built-in
sidebar toggle button
== Relation managers on top ==
EditWorkOrder now enables Filament's combined-tabs mode:
- hasCombinedRelationManagerTabsWithContent() → true
- getContentTabPosition() → ContentTabPosition::After
- getContentTabLabel() → "ⓘ Info & antet"
Result: the WO edit page opens on the Manopere tab (first tab).
Piese, Subcontract, Plăți follow. The full form (Antet, Diagnostic,
Foto, Tracking & ETA, Plată & total) is the last tab, opened only
when the user needs to change header info.
Rationale: mechanics and receptionists spend 90% of their WO edit
time in Manopere/Piese, not in the header. Putting those first cuts
one scroll on every WO open.
== Compact header ==
WorkOrderResource::form restructured:
- "Antet fișă" section: single compact section with 4-column dense
grid. Contains only the always-visible essentials: Nr., Deschis,
Status, Urgență, Client (span 2), Auto (span 2), Maistru (span 2),
Km intrare, Km ieșire. Uses ->compact() to reduce padding.
- Diagnostic / Foto / Tracking & ETA / Plată & total: ALL now
->collapsible()->collapsed()->compact(). They appear as closed
accordions — visible titles but zero screen space until clicked
open.
Result: opening the "Info & antet" tab shows a tight 4-column top
row + 4 closed accordion titles below. Fits in ~40% the vertical
space of the previous layout.
== Sidebar collapsible on desktop ==
TenantPanelProvider ->sidebarCollapsibleOnDesktop() — Filament adds
a chevron button that toggles the left nav between full-width labels
and icon-only strip. Persisted per user via localStorage.
Result: user can narrow the left column with one click when they
want more horizontal space for a wide Manopere table.
No schema changes. No test changes required (existing WO tests still
pass — 303/303).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client sees only Total. Salary is calculated from salary_base = client_price
× (1 − margin/100). Margin never appears in customer-facing surfaces (PDF,
tracking JSON, portal).
Terminology: "marjă internă" — internal profit margin. NOT VAT/TVA. Never
called NDS/TVA anywhere in the code to avoid confusion with real Moldova
tax reporting (Doc 19/1C integration).
== Configuration ==
Fallback chain (in MarginResolver::resolve):
1. WorkOrder.override_margin_pct — per-Fișă for special contracts/VIP
2. User.internal_margin_pct — per-mechanic (main setting)
3. Company.settings.default_internal_margin_pct — tenant default
4. 0.0 — no margin
Example (mechanic Andrei with 20% margin):
User enters price_per_hour = 250 for 1h diagnosis
→ total = 250 (what client sees, goes into PDF)
→ salary_base = 250 × 0.80 = 200 (what mechanic gets salaried on)
→ applied_margin_pct = 20 (frozen)
If admin later changes Andrei's margin to 40%, the row's salary_base does
NOT change — history is immutable. Only new rows use the new margin.
Solves the retroactive-recompute problem for closed payroll periods.
== salary_base freeze semantics ==
wo_works gains 2 columns:
salary_base decimal(10,2) nullable
applied_margin_pct decimal(5,2) nullable
Frozen at save time by WorkOrderWork::saving hook. Recomputes only if
total OR master_id changes (i.e., someone actively edits the price or
reassigns the mechanic — in those cases we WANT the salary_base to
follow). Legacy rows (before this feature) have null salary_base;
PayrollCalculator falls back to total for them.
== PayrollCalculator uses salary_base ==
Previously: sum(wo_works.total) × works_pct → gave the mechanic a cut
of the price INCLUDING margin.
Now: sum(salary_base ?? total) × works_pct → the cut is from the
labor rate excluding margin.
Impact: for a 250 lei diagnosis at 20% margin with 50% payroll cut, the
mechanic gets 200 × 50% = 100 lei (was 250 × 50% = 125 lei). The shop
keeps the 50 lei margin regardless of the payroll %.
== RBAC gate ==
New permission FINANCE_VIEW_INTERNAL_MARGIN. Assigned to owner + admin +
manager + accountant in seed matrix. Not granted to mechanic,
receptionist, or viewer — those roles never see the "Bază salariu"
disclosure line or the margin % fields.
== UI surfaces ==
UserResource — new "Salariu & marjă" section (visible only with
FINANCE_VIEW_INTERNAL_MARGIN):
- Tarif orar (MDL)
- Marjă internă (%) with helper text explaining the -X% semantics
- Placeholder tells manager the exact formula
WorkOrderResource form — new override_margin_pct field in the "Plată &
total" section, gated by same permission. Helper text: "Doar pentru
cazuri speciale. Lasă gol pentru a folosi marja mecanicului."
WorksRelationManager (WO edit page) — Total column now shows a gray
subtitle line "Bază salariu: 200.00 MDL · marjă 20%" ONLY for users
with FINANCE_VIEW_INTERNAL_MARGIN. Everyone else sees just Total.
== Contract tests: NO leak ==
InternalMarginTest verifies with black-box grepping that:
- WorkOrderPdfService::generate output contains NONE of
{salary_base, internal_margin, applied_margin_pct, marja intern,
Bază salariu}
- /api/track/{token} JSON payload contains NONE of the same terms
- wo_parts table has no salary_base column (margin ONLY on labor)
- Changing mechanic.internal_margin_pct after work is saved does NOT
rewrite the historical salary_base (frozen)
- WO override wins over mechanic margin (contract-priced clients)
- Fallback chain: WO → mechanic → company default → 0
== Suite ==
298 passed (828 assertions). Was 285. +13 InternalMarginTest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema:
- subcontractors (specialty, rating, contact)
- subcontract_jobs (work_order link, cost, markup_pct, client_price, status
workflow, sent_at/eta/returned_at, paid_to_sub)
Models:
- SubcontractJob: auto number (SC-YY-NNNN), client_price = cost×(1+markup/100)
when markup>0 (else manual), margin() helper, recalcs parent WO on save/delete
- WorkOrder.recalcTotal now includes non-cancelled subcontract job client_price
Filament (new "Subcontractare" nav group):
- SubcontractorResource (specialty/rating CRUD)
- SubcontractJobResource board with cost/client/margin columns + status filters,
nav badge = open jobs
- SubcontractJobsRelationManager on WorkOrder
Tests (7 new):
- client_price from markup; manual price without markup; auto number;
WO total includes jobs; cancelled excluded; delete recalcs; tenant isolation
Closes roadmap to 16/18 stages (only Stage 10 Bodyshop remains).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contextual multipliers layered on top of base MarkupRule pricing, applied
per work-order line based on vehicle, client and urgency.
Schema:
- pricing_coefficients (multiplier, conditions JSON, priority, stackable)
- vehicles.vehicle_class (sedan/suv/commercial/hybrid/ev/premium)
- clients.is_vip
- work_orders.urgency (normal/urgent/express)
PricingEngine::quote(Part, Vehicle?, Client?, urgency):
- base = MarkupRule on buy_price (fallback sell_price or buy×1.30)
- context: class (explicit or inferred hybrid/ev from fuel), age, vip, urgency
- stackable coefficients all multiply; non-stackable take only the highest
- returns {base, final, applied[]} breakdown
PricingCoefficient::matches(ctx) — classes/age range/vip/urgency conditions
(empty = always applies).
Filament:
- PricingCoefficientResource with condition builder (classes, age, vip, urgency)
- vehicle_class select, client is_vip toggle, WO urgency select
- "Preț inteligent" action on WO parts shows breakdown + applies sell_price
Tests (6 new):
- base-only without coefficients; age coefficient gating; VIP; express urgency;
stackable multiply vs non-stackable highest-wins; hybrid inferred from fuel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 3-step onboarding wizard at /app/onboarding (auto-redirected via
RequireOnboarding middleware on first login per tenant)
- Empty states with icon + heading + description on Client, Vehicle,
WorkOrder, Lead, Part lists
- Docs: operations/{api,i18n,2fa,monitoring}.md, stack/reverb.md
- Updated 00-index.md and journal.md with status of all 15 items
- barryvdh/laravel-dompdf instalat
- WorkOrderPdfService: încarcă WO cu toate relațiile (works/parts/payments),
embed-ează logo ca data URI, foloseste theme_color din settings
- Blade template /resources/views/pdf/work-order.blade.php:
- Header cu logo + date companie + nr fișă + data
- Box-uri client + auto (kilometraj/VIN/plate)
- Plângere + diagnostic
- Tabel manopere (h, preț/h, total) cu maistru pe fiecare rând
- Tabel piese (cod, brand, qty, preț, total)
- Box total cu discount + plăți efectuate + rest de achitat
- Block recomandări cu fundal galben (warning)
- Linii semnătură client + maistru
- Footer cu timestamp generare
- Action 'PDF' (icon descărcare) pe rând în lista de WO
- Action 'Descarcă PDF' în header-ul paginii Edit WO
Schema:
- payments: client_id, work_order_id, user_id (operator), paid_at, amount,
method (cash/card/transfer/mobile), reference, notes
- expenses: supplier_id, purchase_id, paid_at, category (salary/purchase/rent/
utilities/advance/tax/fuel/tools/marketing/other), name, amount, method, ref
Logică auto:
- Payment::saved/deleted recalculează automat work_order.pay_status
(unpaid → partial → paid) based on suma totală vs work_order.total
- WO model are noi metode: payments(), paidAmount(), balanceDue()
Filament resources (group Finanțe):
- PaymentResource: form cu legare opțională la WO + client; tabel cu
Sum summary, filtre azi/luna_curentă/method
- ExpenseResource: 10 categorii preset, badge categ, total summary,
filtru luna curentă
- PaymentsRelationManager pe WO: "Plăți" tab cu auto-fill client_id +
user_id la creare
Widget FinanceOverview:
- Încasări (luna), Cheltuieli (luna), Profit (luna), Datorii clienți
- color coded: profit verde sau roșu, datorii galben/verde
Settings page fix (Filament v5):
- mount() folosește acum $this->form->fill([...]) în loc de $this->data direct
- Filament v5 cere fill explicit pentru a inițializa state-ul schemei
Seed:
- 1 plată parțială pe fișa BMW (200 din 750)
- 6 cheltuieli demo: 3 salarii, chirie, electricitate, achiziție piese
Total Filament tenant routes: 69.
Schema:
- users + specialization, color, hourly_rate (pentru maistri)
- labors: catalog manopere standard cu category/ore/preț (RO+RU)
- work_orders: nr unique per tenant, status workflow (9 stări),
pay_status (3 stări), client/vehicle/master/deal/appointment refs,
complaint/diagnosis/recommendations, total auto-calculat
- wo_works: manopere per fișă, recalc auto la save/delete
- wo_parts: piese per fișă (free-text deocamdată), discount/total auto
Filament resources (group Service):
- LaborResource: CRUD + grupare pe categorie + filter active
- WorkOrderResource: form complex în 4 secțiuni (antet, diagnostic, plată)
+ 2 RelationManagers (Works, Parts)
- MasterResource: vedere User filtrată role=mechanic, edit specializare/
culoare calendar/tarif oră
Conversie auto: la adaugare manoperă din catalog Labor,
form populează numele + ore + preț/oră derivat (price/hours).
Number generator pentru WO: format WO-{YY}-{NNNN} per tenant per an,
calculat în CreateWorkOrder via WorkOrder::generateNumber().
Seed extins:
- 3 mecanici (Vasile/Andrei/Nicolae) cu culori + specializări
- 10 manopere standard din prototipul AutoCRM.html
- 1 fișă demo (BMW X5 plăcuțe Brembo) cu 1 manoperă + 1 piesă, total auto