Commit Graph

128 Commits

Author SHA1 Message Date
Vasyka 6de9c615f7 fix(injector-protocols): derive protocol prefix from tenant slug (not hardcoded PS)
'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>
2026-07-26 11:52:14 +00:00
Vasyka 47eb4f62c1 feat(injector-protocols): full module — resource + PDF export via Browsershot
Implements the injector diagnostic protocol module per spec:
- Migration: injector_protocols + injector_protocol_rows with company_id
  (multi-tenant), vehicle_id (not car_id — this CRM uses Vehicle),
  master_id FK to users, snapshot fields (car_model/plate/vin/mileage/year/
  injector_brand) captured at protocol time.
- Models with BelongsToTenant; auto-generate protocol_number as
  PS-{tenantId}-{year}-{seq6}; auto-seed 8 empty rows on create.
- Permissions: injector_protocols.view + injector_protocols.conclude.
  Default roles: owner/admin (both), manager (both), receptionist (view
  only), mechanic (both). Test permission count updated 52→54.
- InjectorProtocolResource under Service nav group with 4 sections:
  data auto+client (with client_id/vehicle_id lookups auto-filling
  snapshot fields), reason checkboxes, 8-row repeater for measurements
  (Repeater with position hidden; fixed 8, non-addable/deletable),
  conclusion + comment + master signature.
- Table with columns, badge filters, PDF action.
- Blade PDF template: pixel-close to reference (graphite/gold/blue
  brand tokens, striped rows, rotated 'ФОРСУНКИ' header).
- Route /app/injector-protocols/{id}/pdf with permission gate,
  streams PDF via new InjectorProtocolPdfService (Browsershot).
- Dockerfile: install nodejs 22 + chromium + noto fonts + puppeteer-core
  in /opt/browsershot; env vars for Browsershot binary paths.
- +71 RU/EN translations covering resource + PDF template.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-26 11:27:53 +00:00
Vasyka de2964c854 i18n(wo relations): translated snapshot names + explicit empty states
- 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>
2026-07-26 10:59:47 +00:00
Vasyka 9609295a9e feat(labors): trilingual name — add name_en column + Labor::label() helper
- 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>
2026-07-26 10:54:00 +00:00
Vasyka 64a170b7f9 i18n(work-order works): translate labor Select category + h suffix + modal titles
- 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>
2026-07-26 10:47:00 +00:00
Vasyka 1f471cc1ea i18n(dates): use app locale for month names on MechanicKpi + PDF appointments
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>
2026-07-26 10:43:54 +00:00
Vasyka 6b5ab02e68 i18n(masters): translate Specializare column tokens (Edit was skipped)
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>
2026-07-26 10:42:26 +00:00
Vasyka 8b77d8ae7e i18n(calendar-board): translate mechanic specialization tokens in matrix + legend
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>
2026-07-26 10:37:58 +00:00
Vasyka d97b9b3d6d i18n(settings): wrap delivery methods CheckboxList with I18n::opts + 3 keys
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>
2026-07-18 11:36:10 +00:00
Vasyka a6f21762fc i18n(settings): fix currency I18n::opts + push notification + closing-quote strings
- 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>
2026-07-18 11:33:52 +00:00
Vasyka 9296d9db11 i18n(permission-overrides): explicit empty state (was auto-generated class name)
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>
2026-07-18 11:24:41 +00:00
Vasyka 07398dcbf4 fix(roles): actually apply permissionFields human labels (Edit was silently skipped)
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>
2026-07-18 11:20:44 +00:00
Vasyka 49b491a7bd feat(rbac): human labels + descriptions for all 50 permissions
- 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>
2026-07-18 11:16:21 +00:00
Vasyka af0327d27d i18n(permission-overrides): wrap DENY option + explicit CreateAction labels
- '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>
2026-07-18 11:11:37 +00:00
Vasyka 3d60e1d368 i18n(users): translate 'status' badge column + relabel Locale → Язык / Language
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>
2026-07-18 11:08:46 +00:00
Vasyka 58f168dd97 i18n(users): wrap role Select options with I18n::opts + 2FA status + Locale
- 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>
2026-07-18 11:04:30 +00:00
Vasyka 78e48c6dda i18n(reports): wrap Anul curent + auto-translate specialization tokens
- Reports.php periods() had 'this_year' hardcoded; now wrapped in __()
- masters tab specialization cell: split by '/' and __()-translate each
  token so 'Motor / Cutie viteze' auto-renders as «Двигатель / КПП»
  on RU (and 'Engine / Gearbox' on EN). Users can enter any free-text
  specialisation; the common keywords now have RU/EN translations.
- +12 specialisation vocabulary entries (Motor, Frâne, Suspensie,
  Anvelope, Cutie viteze, Electrică, Diagnosticare, Vopsitorie,
  Tinichigerie, Aer condiționat, Roți, Ambreiaj).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-18 10:32:06 +00:00
Vasyka 8e4b1f713e i18n(reports): +3 wrap missed tabs (masters/works/parts) + translations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-18 10:28:55 +00:00
Vasyka 749ce99c4c ui: set panel max content width to Full on both Tenant and Central
Previously .fi-main was capped at width-7xl (~1280px). Tables with
many columns overflowed and needed inner scroll. Now the entire main
area uses 100% width so tables render without truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-18 09:47:52 +00:00
Vasyka 98f71d782c i18n(payroll-runs): +8 translations + fix hardcoded notification title
Notification 'Calculat salariul YYYY-MM pentru N utilizatori' was
built via string interpolation → couldn't translate. Now uses __()
with :period / :count placeholders. Adds 7 more translations for
section titles and column labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-17 07:01:42 +00:00
Vasyka 4ba003f25c i18n(payments): explicit ->label('Metodă') on method select
Field 'method' auto-generated 'Method' as label. Since RO is the source
language, __('Method') fell back to English on RO locale. Now explicitly
labeled with 'Metodă' so all 3 langs resolve properly (Metodă / Метод /
Method).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-17 06:36:49 +00:00
Vasyka 2b56bbc35b i18n(finance): full page — tabs/periods/pnl/balance + Expense.CATEGORIES
- All 4 tabs (Overview/Cashflow/P&L/Balanță) built with __() around
  the text portion, emoji stays in code
- 4 period presets wrapped with __()
- P&L table Marjă/Marjă piese footer wrapped
- Expense::CATEGORIES values look up through __() in the expensesByCat
  loop so categories show translated (Salariu → Зарплата etc.)
- +25 translations covering all UI copy on /app/finance

306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-17 06:28:53 +00:00
Vasyka f45df71410 i18n(pricing-coefficients): wrap BODY_TYPES/TRANSMISSION_TYPES CheckboxList options
The two CheckboxList options passed raw Vehicle::BODY_TYPES and
Vehicle::TRANSMISSION_TYPES arrays — now they run through I18n::opts()
so the RO values translate at display. +6 translations for the enum
values that weren't yet in dict (Crossover, Pickup, Minivan, DSG,
DCT, AMT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 20:59:33 +00:00
Vasyka 2985c38d93 i18n(markup-rules): +11 translations + wrap key label & categories
Wrapped the dynamic Select 'key' label (Brand/Categorie) with __(),
switched the category options to I18n::opts(). Notification 'Recalculat
preț pentru N piese' now uses __(:n) placeholder.

+11 keys: regulă, reguli markup, Cheie, Mai mic = aplicat primul.,
'Va recalcula sell_price…' confirm text, notification, 'Interval preț'
enum, and missing Part.CATEGORIES (Ulei, Filtre, Lichide, Distribuție).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 20:55:13 +00:00
Vasyka 00fb4a304a feat(units): add Unit-of-measure nomenclator (per-tenant, 3 langs)
- Migration: create `units` table + nullable unit_id FK on
  parts / wo_parts / purchase_items / labor_parts. Seeds ~11 standard
  units (buc/set/l/ml/kg/g/m/cm/m²/oră/pack) for every existing
  tenant and backfills unit_id by matching the legacy string `unit`
  column. Keeps `unit` string as fallback so old code paths keep
  rendering.
- Unit model: label(locale), forSelect(locale), labelFor(id, code)
  helpers. All 4 owner models get unitModel() relation + unitLabel()
  accessor.
- UnitResource under Depozit group with Filament UI: code, sort,
  is_active + separate name_ro/name_ru/name_en fields.
- Filament forms/tables updated: Part / PurchaseItem / LaborPart /
  WorkOrderPart now use Select('unit_id')->options(Unit::forSelect())
  for input and TextColumn->getStateUsing(unitLabel()) for display.
  Selecting a part auto-fills unit_id when the source has one.
- +10 translations for the new resource + defaults; nav.label
  'Unități de măsură' added to all 3 lang files.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 20:52:06 +00:00
Vasyka c864b112b5 fix(purchases): actually apply CreateAction override
Previous commit added the translation entries but the Edit call
failed silently (file-not-read guard). Now the CreateAction has
->label(__('Adaugă articol')) + ->modalHeading(__('Adaugă articol
în comandă')) so RU/EN see translated modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 20:27:34 +00:00
Vasyka e680b2e63c i18n(parts-edit): override CreateAction, +19 translations for tabs/columns/empty
- CrossRefsRelationManager: explicit CreateAction/EditAction labels &
  modal headings (was auto-generated as 'Create Part Cross Ref')
- +19 translations for PartResource sections (Prețuri, Furnizor
  preferat), photo gallery helper, relation-tab titles (Loturi FIFO,
  Coduri cross), Batches columns (Intrat/Rămas/Preț unit.), CrossRefs
  column (Cod echivalent), all empty-state headings/descriptions on
  the 3 relation managers, Val. abbreviation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 11:10:09 +00:00
Vasyka f7f8a67a1b i18n(service-templates/items): override CreateAction + emptyState + edit modal
Filament's default empty state used the model class name ('service
template items') and default 'Create service template item' modal
heading. Both are now explicit __() calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 09:50:31 +00:00
Vasyka c16624ebe3 fix(labor-parts): actually apply modal-heading overrides
Previous commit's translation entries were added but the source change
was silently rejected by the Edit tool's file-not-read guard. Now the
CreateAction/EditAction overrides are in place; RU shows 'Добавить
запчасть по умолчанию' / 'Изменить запчасть по умолчанию' instead of
the auto-generated 'Create Labor Part'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 08:05:47 +00:00
Vasyka e969bb9c7d i18n: wrap all formatStateUsing(fn(\$s)=>X::CONST[\$s]??\$s) with __() (21 files)
Previous sed pass only matched \$state; missed \$s and other arg-name
variants. This time the regex is arg-name-agnostic and touches 21
files across Filament resources & relation managers.

Also wraps two special cases: UserResource role-labels lookup and
LaborResource pricing_mode ternary ('Fix' | 'Pe oră').

+27 human translations for the enum values that were still identity
fallback: WorkOrderWork.STATUSES (De făcut), Purchase.STATUSES,
OnlineOrder.STATUSES, Call.DIRECTIONS/STATUSES, BodyshopJob.TYPES/
STATUSES, TireSet.SEASONS, MessageTemplate.CHANNELS,
DamagePoint.SEVERITIES, ServiceTemplateItem.KINDS.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 07:41:35 +00:00
Vasyka e4e63a3a09 i18n(work-order-edit): translate tab, tracking-link modal + widen for QR
- getContentTabLabel: 'ⓘ Info & antet' now goes through __()
- Tracking-client modal heading translated ('Tracking client — WO #…')
- Both 'Închide' cancel labels wrapped in __()
- Tracking modal widened to 'lg' so the QR + link input fit without overlap
- Notification 'Șablon aplicat' template built with __() so RU/EN see
  translated pieces
- +16 translations covering the QR modal, Info tab, Închide etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 07:35:56 +00:00
Vasyka ec25a351ad fix(work-order): actually wrap PricingCoefficient::URGENCY with I18n::opts
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>
2026-07-16 07:29:04 +00:00
Vasyka f3d25952cc fix(finance): guard max() when byDay array is empty
max(...[]) throws ArgumentCountError. Split the intermediate array
and only call max() when it has elements. Fixes /app/finance 500
on tenants with no payments in the current period.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 07:17:10 +00:00
Vasyka f6364176ff i18n(vehicle-create): +19 translations, wrap fuel/class options with I18n::opts
- Fuel select was passing raw strings ('Diesel', 'Hybrid', 'EV', 'Electric'
  hardcoded); rewrote as DB-key => RO-label pairs run through I18n::opts
- vehicle_class select now uses I18n::opts(PricingCoefficient::VEHICLE_CLASSES)
- +19 RU/EN entries: section titles (Identificare, Tehnice), field labels
  (Marca, Kilometraj, Clasă…), helper texts, vehicle-class enum values
  (Compact/SUV/Van/Commercial/Premium/Sport/Camion/Cabrio/Motocicletă),
  fuel enum values (Motorină/Hibrid/Electric/GPL/GNC), auto-generated
  headline keys (Fuel/Engine/Gearbox/Color).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 07:08:13 +00:00
Vasyka a966e0b2c7 i18n(calendar): translate 'h/zi', matrix howto, load legend, day-count word
- Post/mechanic meta 'h/zi' → __('h/zi') so RU shows 'ч/день', EN 'h/day'
- Wrap the 3 load-legend labels (0–5h/10, 5–8.5h/10, ≥9h/10) so 'h' → 'ч' on RU
- Split the long howto sentence so 'Pod'/'Mecanic' + 'Zile' translate
- Default fallback 'Pod 1 (default)' now uses __('Pod') + __('implicit')

Adds 5 new translations. Post NAMES themselves are DB rows (user data)
and are shown as-is — they're not translated by the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 06:59:45 +00:00
Vasyka 8cf14b7e00 fix: add missing App\Support\I18n class
Was created but never staged in the previous commit; deployment failed
with 'Class App\Support\I18n not found' on any page that uses the
I18n::opts() wrapper on Model::CONSTANT Select options.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 06:54:28 +00:00
Vasyka 95aeecb932 i18n: fix widget heading, translate Select options from model consts, +40 keys
- LowStockTable: rename getHeading → getTableHeading (correct Filament API)
- Global I18n::opts() helper wraps Model::CONST values in __() so
  Select dropdowns show translated status/type/season/etc. labels
- Sed-transform ->options(X::CONST), ->options(array_combine(X::A,X::A)),
  and formatStateUsing(fn($s)=>X::CONST[$s]??$s) → wrap with __()
  (58 options + 10 combines + 7 formatStateUsing across 28 files)
- CalendarBoard: all 7 day names now via __() (was missing 4)
- calendar-board.blade: fix untranslated 'săptămâna curentă', 'capacitate',
  'rata confirmare', 'mediu', 'plin', 'Pod / Zi' / 'Mecanic / Zi'
- +40 RU/EN translations (Client & Auto, Maistru / Mecanic, Programat,
  Sosit, Finalizat, Anulat, Neprezentat, days of week, calendar KPIs)

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 06:42:16 +00:00
Vasyka 38650da875 fix(i18n): auto-translate labels on Filament columns/fields/filters/actions
Root cause of untranslated table headers: Filament's HasLabel::getLabel()
auto-generates a label from the field name (e.g. `name` → "Name") but only
calls __() on it if you also invoke ->translateLabel(). Without that, the
auto-generated label always renders in English regardless of app locale.

Fix: configureUsing() in AppServiceProvider makes translateLabel() the
default for every Column, BaseFilter, Field, Action and BulkAction.

Then added 175 human RU/EN translations for Filament's auto-generated
keys (Name, Phone, Email, Status, Balance, Created at, Plate, Make,
Model, Year, VIN, Mileage, Owner, Client, Vehicle, Amount, Total, Qty,
Actions, Search, Filter, Export, Import, plus enums/relationships).

All 306 tests pass. Deploy will fix table headers on
Clients / Vehicles / Work orders / Payments / Purchases / Suppliers etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-16 05:21:29 +00:00
Vasyka 7077fff3b3 feat(i18n): convert 38 hardcoded static labels + safe blade wrap + 226 keys
- Convert all remaining static \$modelLabel/\$pluralModelLabel to getter
  methods with __() (AppointmentResource, WorkOrderResource, MasterResource,
  ServiceTemplateResource, LaborResource, and 33 others)
- Safe blade wrap v2 (37 files, 201 wraps): fixed the regex to not match
  `->`, `=>`, `!<` — previous aggressive pass broke @if directives
- Bulk-translate lowercase model-label keys (programări, fișe lucru,
  tehnicieni, șabloane servicii, norme-ore, etc.) — these were the RO
  strings appearing in page titles/breadcrumbs
- Add human RU/EN for mechanic-kpi (Mecanic/Lucrări/Norma ore/etc.)
  and calendar-board words

Tests 306 green. Blade compiles cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-15 06:17:10 +00:00
Vasyka 3911012c65 feat(i18n): mega-wrap 494 raw RO strings + 269 lang entries + 161 human RU/163 EN
Broad sweep across the whole codebase:
- Blade views (39 files, 315 wraps): tag-text and title/placeholder/alt
  attributes wrapped with {{ __() }}. Excludes scripts, styles, @php,
  @verbatim, {{ }}, {!! !!}, comments to avoid touching interpolations.
- PHP (54 files, 179 wraps): array 'key' => 'RO value' patterns and
  list items with diacritics wrapped with __(). Reverted __() inside
  const arrays (PHP disallows non-constant expressions).
- Added 269 new keys to lang/{ru,en}.json (identity fallback for
  unknowns → 161 human RU + 163 EN translations added for the most
  common enums, stages, roles, statuses, payment methods, vehicle
  categories, warehouse, portal, form actions.

Missing translations fall back to RO so the UI never breaks. All 306
tests pass; view cache compiles cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-15 05:53:22 +00:00
Vasyka 2d447e6294 feat(i18n): fix widgets, pipeline & mechanic boards + missing translations
Follow-up to previous i18n pass — user reported many visible RO strings on
EN/RU locales because auto-wrap missed:
  - Stat::make() first-arg labels in StatsOverview, FinanceOverview, PlatformStats
  - $heading static in LowStockTable (converted to getHeading() method)
  - Pipeline board Blade view: 40+ literal strings wrapped with {{ __() }}
  - Mechanic board Blade view: statuses, buttons, block modal
  - PipelineBoard.php hardcoded tag labels, stage labels, notify strings
  - MechanicBoard.php confirmBlock notification

Added human RU + EN translations for the visible strings shown in the
screenshots (dashboard widgets, Client/Vehicle list breadcrumbs & columns,
pipeline board topbar/stats/filters/cards/detail panel/quick actions,
mechanic board stats/statuses/block modal). All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-15 05:31:26 +00:00
Vasyka f7fc69077b feat(i18n): wrap 1103 hardcoded UI strings across Filament with __()
- 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>
2026-07-15 05:04:24 +00:00
Vasyka 78ff8d4b43 feat: i18n on Filament admin sidebar — 54 resources/pages translated
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>
2026-07-13 20:41:55 +00:00
Vasyka a03561eaaf fix: SetLocale reads app_locale cookie first (partial from previous commit)
The SetLocale::resolve() edit was dropped from the previous commit.
Cookie priority is essential for the switcher to actually take effect
— without this the /locale route was setting a cookie that nothing
read.

Now resolve order:
1. app_locale cookie (persistent, survives session regen)
2. session locale
3. auth user.locale
4. tenant.settings.language
5. hard-coded 'ro'

Suite: 306 unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-13 20:25:25 +00:00
Vasyka b9d096fa59 fix: language switcher works — GET route, simple links, forced RO fallback
The previous POST-based switcher failed silently: CSRF token check
returned 419 when clicking, and the session never persisted.

Root causes:
- POST requires @csrf token, but the test path fetched pages that had
  no matching form so the token in the DOM didn't match the session
- Some tenant subdomains had SESSION_DOMAIN scoped differently, so
  the cookie set by POST didn't come back on the follow-up GET
- Prod .env had APP_LOCALE=en which took precedence over the config
  edit; when session had no locale yet, defaulted to English

Fixes:

1. Route accepts BOTH GET and POST via Route::match(['get', 'post']).
   Setting your own language is not a security concern — GET is fine.

2. Route explicitly calls $request->session()->save() before redirect,
   forcing the session store to write before the redirect fires.
   Also honors ?redirect=<url> query so the user lands back on their
   original page rather than referer-guessing.

3. lang-switcher partial rewrites to plain <a href> tags (no @csrf,
   no forms). Each link points at /locale/{code}?redirect={current-url}
   so the switch happens in a single hop with predictable target.

4. SetLocale middleware hard-codes 'ro' as the ultimate fallback,
   ignoring config/env. The Romanian portal is the default
   client-facing surface; if a client has no session locale set and
   no user account, they see Romanian (safer than English which has
   no portal translations).

Suite: 306 passed (853 assertions). Unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-13 20:17:12 +00:00
Vasyka 7769ab7737 fix: hide margin text + recompute lines when WO.apply_margin flips
Two related fixes for the WO-level "Aplică marjă internă" toggle:

== 1. Hide description text when WO toggle is off ==

WorksRelationManager's Total column showed a gray subtitle line
"Bază salariu: 320 MDL · marjă 20%" that persisted even after
apply_margin was toggled OFF at the WO level. Confusing — the user
expected "off means invisible".

Fix: description callback now short-circuits to null when
$record->workOrder->apply_margin === false, hiding the entire text.
Also hides when applied_margin_pct is 0 (nothing meaningful to show).

Result: OFF at WO level → zero margin details anywhere in the
Manopere tab. ON → same as before.

== 2. Auto-recompute salary_base on all lines when toggle flips ==

Previously, salary_base was frozen at line save-time. Flipping
apply_margin from on→off left existing lines with the old
20%-reduced salary_base, so payroll still used the reduced amount
even though the user had visually decided "no margin".

Fix: WorkOrder::updated hook detects wasChanged(['apply_margin',
'override_margin_pct']) and iterates through works():
- apply_margin=false → salary_base = total, applied_margin_pct = 0
- apply_margin=true  → resolver chain (WO override → mechanic → default)

saveQuietly() on each line so we don't retrigger the works() booted
hooks that would recompute again.

This is DIFFERENT semantic from user.internal_margin_pct changes —
those DON'T rewrite history (test still passes). The distinction:
- User margin change: personnel decision, must not touch closed WOs
- WO apply_margin change: explicit per-Fișă decision, must affect
  every line on that same Fișă

InternalMarginRecomputeTest (3):
- Flipping WO.apply_margin off recomputes both existing lines to at-cost
- Flipping back on recomputes to margined
- Changing WO.override_margin_pct recomputes with new % (40 → 60% base)

Suite: 306 passed (853 assertions). Was 303. +3 recompute tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-13 20:07:51 +00:00
Vasyka 57bdc1b594 feat: language switcher on portal + apply_margin re-added to Plată & total
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>
2026-07-13 19:42:00 +00:00
Vasyka 113610ea8f feat: WO apply_margin at fișă level + full RO/RU i18n on client portal
Two changes in one commit:

== 1. Moved apply_margin toggle from line-level to Fișă-level ==

The per-manoperă apply_margin toggle is gone from the Manopere tab.
In its place: a single "Aplică marjă internă" toggle in Fișa's
"Plată & total" section (next to override_margin_pct). One decision
per Fișă instead of per line — cleaner mental model, matches how the
shop actually thinks about at-cost vs. billable work.

Migration: work_orders.apply_margin boolean default true (idempotent).
WorkOrderWork::saving now reads WO.apply_margin from DB directly (not
via belongsTo cache) to determine salary_base:
  - WO.apply_margin=false → every line gets salary_base=total, applied_margin_pct=0
  - WO.apply_margin=true  → resolver chain (WO.override → user margin → tenant default)

Old wo_works.apply_margin column stays untouched (backward-compat with
existing rows), but no longer exposed in UI. Tests updated to new
semantic. All existing tests green.

== 2. Full i18n audit on client-facing portal — RO/RU separated ==

Problem: user selecting Russian saw Romanian mixed into headings,
buttons, labels. Every client-facing Blade file was 100% hardcoded
Romanian — zero __() calls.

Fix: created lang/ro/portal.php + lang/ru/portal.php with 131 keys
across 3 namespaces:
  - portal.common (email, phone, save, total, powered_by, ...)
  - portal.invitation (welcome_name, activate_account, expired_body, ...)
  - portal.tracking (title_fisa, approve, approval_needed_title,
                    ready_estimated, hours, unit_pcs, ...)
  - portal.shop (catalog, cart, checkout_title, order_number, vin_title,
                signin_title, add_to_cart, in_stock, ...)

Converted 15 Blade files to __() calls:
  - resources/views/invitations/{accept,expired,invalid}.blade.php
  - resources/views/tracking/show.blade.php
  - resources/views/shop/{layout,catalog,cart,checkout,order,account,part,vin}.blade.php
  - resources/views/shop/auth/{login,register,forgot,reset}.blade.php

Each view's <html lang="{{ app()->getLocale() }}"> now reflects the
resolved locale (was hardcoded lang="ro").

SetLocale middleware resolves locale in this order:
  1. session locale (user picked via language switcher)
  2. authenticated user.locale
  3. tenant.settings.language
  4. app.locale default (now 'ro')

Config change: config/app.php default locale + fallback both = 'ro'
(was 'en'). English falls back to Romanian for portal.* keys since
we don't ship English portal translations — a Romanian shop that
switches to English shows Romanian text, which is safer than showing
"portal.invitation.activate_account" literals.

phpunit.xml sets APP_LOCALE=ro so test assertSee() calls that look
for Romanian text pass.

Verified via portal.* grep: 131 __() calls across 15 files. Zero
hardcoded Romanian nouns/verbs left in any client-facing view.

Suite: 303 passed (840 assertions). Unchanged count — refactor,
not new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-13 04:56:04 +00:00
Vasyka f5ff3f149a feat(WO edit): manopere/piese first + compact header + collapsible sidebar
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>
2026-07-07 19:52:30 +00:00
Vasyka f4ccc306dc feat: marjă internă — Settings % + toggle per manoperă + visibility flag
Adds three usability improvements to the marja internă feature:

1. Settings UI section "Marjă internă (nu TVA)" — configurable at tenant level
2. Per-line toggle "Aplică marjă" on each manoperă
3. Global visibility flag to hide margin details in-session

== 1. Settings page ==

New section on /app/settings (gated by FINANCE_VIEW_INTERNAL_MARGIN):
- "% marjă implicit" numeric input with % suffix, 0–90 range
- "Afișează detalii marjă la procesele calculate" toggle (default on)
- Explicit label "Marjă internă (nu TVA)" plus helper text explaining
  it's not the Moldovan tax — feeds into MarginResolver as the tenant
  default, applied only when the mechanic has no per-user margin.

Persists as company.settings.default_internal_margin_pct and
company.settings.show_internal_margin_details.

== 2. Per-line "Aplică marjă" toggle ==

New wo_works.apply_margin boolean, default true. When false:
  applied_margin_pct = 0
  salary_base = total   (mechanic gets salaried on the full amount)

Use case: oil change, tire mount, and similar "at-cost" services where
the shop doesn't want to hold back part of the labor rate. The owner
can flag those specific lines while keeping margin on diagnostic and
premium labor.

WorksRelationManager form gains a Toggle field (gated by
FINANCE_VIEW_INTERNAL_MARGIN); table gains a ToggleColumn for quick
inline flipping without opening the row.

Booted hook now recomputes salary_base when apply_margin is dirtied,
so toggling live in the table takes effect immediately.

== 3. Show internal margin details flag ==

Global tenant flag (default on): when off, the gray subtitle line
"Bază salariu: 200 · marjă 20%" under the Total column disappears for
everyone, even users with FINANCE_VIEW_INTERNAL_MARGIN.

Practical use: when reviewing a Fișă face-to-face with the client on
the manager's screen, flip the flag off from Settings for the day →
no risk of the client accidentally seeing internal numbers. Flip back
when done.

The flag lives in company.settings.show_internal_margin_details.

== Description text on the Total column ==

Now shows either:
- "Bază salariu: 200.00 MDL · marjă 20%" when apply_margin=true
- "Fără marjă · bază salariu = Total" when apply_margin=false
- nothing when show_internal_margin_details=false or role lacks permission

== Tests ==

InternalMarginToggleTest (5):
- apply_margin=false → salary_base equals total, applied_margin_pct=0
- apply_margin=true (default) still applies 20% margin
- Toggling apply_margin recomputes salary_base bidirectionally
- Company default margin resolves when mechanic has no per-user setting
- show_internal_margin_details flag persists correctly in Company.settings

Suite: 303 passed (845 assertions). Was 298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-07 19:39:51 +00:00