- 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>
- 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>
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>
- client_id field: switched to relationship() with createOptionForm so new clients
can be created inline (name, phone, email); search also matches by phone
- vehicle_id field: added createOptionForm (make, model, plate, year, VIN); newly
created vehicle is automatically linked to the currently selected client
- master_id field: now shows only active users with role mechanic/master
- WorksRelationManager: labor catalog select marked optional; added createOptionForm
so new catalog entries can be added without leaving the WO form
- SubcontractJobsRelationManager: category table column now translates value via __()
and shows explicit label(__('Categorie'))
- PDF work-order: replaced 📞 / ✉ / ⚠ emojis with plain-text equivalents so
DomPDF/DejaVu Sans renders them correctly instead of showing "????"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FAQ sections were only 4 (service/crm/depozit/finante) even though
the sidebar has 8. Added the missing 4 to Hints::AREAS with proper
icons + accent colors:
📊 Analiză (pink) 📣 Marketing (orange)
🛒 Magazin (cyan) ⚙️ Admin (slate)
Retagged 15 hints that were shoved into wrong areas as a workaround
(admin.*/marketing.*/shop.*/analytics.* now use the matching bucket).
FAQ blade reads accent color from the AREAS registry instead of a
hardcoded local map.
Also added per-tenant navigation group ordering:
- Settings → new "Ordine meniu lateral" section with a Filament
Repeater that has reorderable buttons (drag/arrow buttons) but no
add/delete (fixed list of 8 groups)
- Persisted to settings.nav_group_order as an array of keys
- New ConfigureNavGroups middleware (runs after ResolveTenant on
the web stack) reads the current tenant's order and calls
$panel->navigationGroups([...]) so the sidebar renders in that
order per-request
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filament pages wrap their content in an outer <form>. My inline
<form method="POST"> for the hint toggle + reset was nested inside
that outer form — invalid HTML, so browsers dropped my submit and
either did nothing or ran the outer Livewire form's handler.
Replaced both forms with plain <button type="button"> + onclick
fetch() POST + window.location.reload(). Controller now also returns
JSON when Accept: application/json (fetch sends that) instead of a
back() redirect.
The server-side flip was always correct — verified via a Feature
test that posts to /app/hints/toggle and asserts the user column
flipped from true → false.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous pill button showed "● ACTIVE" or "○ INACTIVE" but users read
it as a status label, not a clickable action — nobody clicked to
disable.
New UI: 46×24 iOS-style switch that slides between grey (OFF) and
green (ON) with a sliding white knob, plus a text label next to it.
The whole switch is a form button — clicking flips hints_enabled.
Title attribute + label make both directions obvious: "Click pentru
a DEZACTIVA" when on, "Click pentru a ACTIVA" when off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New tenant setting: settings.faq_video_url. TextInput on Settings
page → Liste configurabile (accepts YouTube watch URL, youtu.be
short URL, YouTube shorts/embed URL, or Vimeo URL).
- Faq page has getVideoEmbedUrl() that converts recognized URLs to
their /embed/ variant. Unknown URLs fall back to a "?" card with
a direct link. Admins see a "configure in Settings" nudge when no
URL is set.
- Video renders as a 16:9 iframe capped at 480px height, above the
hero.
- .faq-shell is now width:100% (was max-width:1200px) so FAQ uses
all available horizontal space.
- .faq-grid switched from strict 2 columns to
repeat(auto-fit, minmax(420px, 1fr)) so wide screens get 3 columns
and narrow screens stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced the flat vertical list with:
- Hero strip (gradient blue) with title, subtitle, and 2 stat pills
showing total topics + section count
- Controls bar in a card: search input with inline icon, pill-shaped
toggle button (green when hints ON), reset button with icon
- 2-column responsive grid of area cards. Each card has a colored
header (service=blue, crm=purple, depozit=amber, finante=green)
with icon + name + count badge
- Details/summary items use +/− indicators, tinted background when
open, hover state; body has pill-styled links and green next-step
- Full dark-mode support
- Empty state with search icon
Same content, much easier to scan and looks like a proper help center.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registry grows from 14 → 25 hints. New entries:
- wo.dashboard.topbar_actions / meta / tabs / history / finance /
timeline / bottom
- wo.create.overview
- crm.clients.overview / vehicles.overview
- finante.dashboard.stats / expenses.overview
"?" icons injected into:
- WO dashboard: top-bar actions, meta header, tabs bar, history card,
finance card, timeline card, bottom action bar (in addition to the
existing dashboard title, PDF preview, chat)
- CalendarBoard: title
- PipelineBoard: title
- Excel import wizard: step 1
- Main tenant dashboard (via PanelsRenderHook::PAGE_START scoped to
Dashboard::class): small banner with a "?" and a shortcut to the
FAQ page for users who don't see the icons
Users now see hint icons on the primary daily-use screens; more can
be added by dropping <x-hint key="..." /> anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Infrastructure for a portal-wide in-app help system:
* Users have hints_enabled (default true) and dismissed_hints (JSON
array) columns. User::shouldSeeHint(key) checks both.
* app/Support/Hints.php — single-source-of-truth registry with 14
pilot hints across Service (5), CRM (3), Depozit (3), Finanțe (3).
Each entry has RO/RU/EN title + body + optional next-step + links.
* <x-hint key="wo.dashboard.overview" /> Blade component renders a
small "?" icon with Alpine.js popover; the popover shows title,
body, next-step, related links and an "X" button that POSTs to
/app/hints/{key}/dismiss.
* HintController handles dismiss (per key), toggle (global on/off)
and reset (clear dismissed + re-enable). Routes are auth:web.
* /app/faq page (Filament Page under Admin group) renders the whole
registry grouped by area with a live search box and buttons to
toggle global hints or reset dismissed ones.
* Wired 3 pilot hints into the WO dashboard: title (overview), Docs
tab PDF preview, and the Chat client card.
Follow-ups: extend registry to cover more pages and add <x-hint>
tags where useful. Filament resource fields can also reuse the same
copy via ->hint()/->helperText().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same Alpine.js overlay pattern as the WO dashboard Docs tab: click
"🖨 PDF programări" opens a centered modal with the PDF iframed at
90vh, plus "Descarcă" and "Deschide în tab nou" fallbacks in the
header and Esc/click-outside to close. iframe src only binds when
the modal is open, so PDF generation doesn't happen on page load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wire:click="exportPdf" attempted to return a binary Response through
Livewire's JSON channel — it re-encoded the PDF bytes as JSON and
triggered "Malformed UTF-8 characters, possibly incorrectly encoded".
Added GET /app/appointments/pdf?from=...&to=... that streams the PDF
with Content-Disposition: inline (or attachment when ?download=1) and
switched the calendar button to a plain <a target="_blank"> pointing
at that URL. The button now includes the currently visible period via
query params.
The old exportPdf() method is retained (still returns a plain PDF
Response) because CalendarEnhancementsTest asserts against it — it's
just no longer wired to the UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WO list row click now opens /work-orders/{id}/dashboard instead of
/edit (via ->recordUrl on the table). Edit remains reachable from
the per-row Filament EditAction.
- CreateWorkOrder redirects to the dashboard after save instead of
the edit page.
- Dashboard top bar exposes: "Listă" (back to WO list), "+ Nou"
(create), and "Editare completă" (full edit form) so users don't
have to hop through the sidebar to move between related WO screens.
The old /work-orders, /create, and /{id}/edit routes stay intact —
just the default navigation flow now converges on the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All "Vizualizare PDF" actions now open a wide (7xl) Filament modal
containing an iframe pointing at the inline PDF endpoint, with a
top-right "Descarcă" and "Deschide în tab nou" fallback.
- WorkOrder edit action + WO table row action
- InjectorProtocol edit action + protocols table row action
- Dashboard Docs tab preview button (Alpine.js overlay since it's
a plain Blade view, not a Filament action)
The underlying /pdf routes stay unchanged (Content-Disposition: inline,
?download=1 for attachment) — the iframe just loads the same URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- Documente tab: link to a new /app/work-orders/{id}/pdf route that
streams the invoice PDF via WorkOrderPdfService; also lists any
files uploaded to the 'signed_documents' media collection.
- Chat client card: only show the input form when telegram_bot or
whatsapp_business is enabled AND credentialed in the tenant's
Integrations page. Otherwise render an empty state with a shortcut
to /app/integrations.
RU + EN translations added for the new strings (RO source).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vehicle model doesn't use Spatie MediaLibrary — the direct call broke
the dashboard for any WO whose vehicle has no MediaLibrary integration.
Guard with method_exists and remove the diagnostic logging + URL echo
now that the routing issue is resolved.
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>
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>
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>
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>
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>
- 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>
The mega-i18n script mis-wrapped a PHP fragment inside the array
literal of a Blade @foreach on reports.blade.php (line 91). It
matched the '>' from '>= 0' as an HTML tag boundary and turned the
rest of the array into a broken __() string, which caused
'unexpected endforeach' compile error.
Rewrote the array with proper __() calls on each label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 4 stepper labels (Upload / Mapare coloane / Previzualizare / Confirmare)
now split so the number stays and only the text runs through __()
- Step 3 heading rewritten with __(':n poziții') placeholder
- 'Selectat: filename.xlsx' hint wrapped
- Row status badges (Găsit/Nou/Nu găsit) split so emoji stays and label
translates
- 'Articole noi (se vor crea)' summary line wrapped
+8 translations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full page translation across all 4 steps (upload / map columns / preview /
done), including form labels, buttons, helper text, empty state, and
final result summary. Notification 'N poziții importate' now built via
__(':n')-placeholder so it translates on RU/EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Wrapped 3 remaining raw RO strings ('An model (estimat)', WMI error
fallback, 'Niciun rezultat pentru') and added human RU/EN for all
15 keys used on /app/vin-search.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Wrap inline blade text ('Mod recepție'), placeholder, and JS error
messages (via @json(__(...)) so they land as JS strings).
- +13 translations covering all UI text on /app/scanner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
Closes 5 user-requested features in /app/calendar-board:
1. View mode switcher: Zi / Săpt / Lună / Custom / Listă
2. Editable post names + assignable default master per bay
3. Quick-add bay (+ Pod nou) from calendar toolbar — supports yard
spaces without a lift ("Curte 1", "Atelier electric")
4. PDF export of programări for printing
5. Inline list view alongside the matrix view
== View modes ==
$viewMode: day | week | month | custom | list
- Day view: 1 column, just today (or navigated day). Shift moves day by day.
- Week view: current 7-column matrix (unchanged default).
- Month view: 30/31 columns shown smaller (70px each). Shift moves by month.
- Custom: 2 date pickers for arbitrary start..end range (max 31 days).
- List view: flat sortable table with Data/Ora/Subiect/Client/Telefon/
Auto/Pod/Maistru/Status columns. Click row → opens detail panel.
getDays() computes the right day count + start anchor for each mode.
setViewMode() snaps weekStart to the right anchor (startOfMonth, today,
startOfWeek). shiftWeek delta semantics adapt: day mode shifts 1 day,
month mode shifts 1 month, others shift 7 days.
== Editable posts + default master ==
New PostResource (/app/posts) in Admin group: full CRUD with name,
color, hours_per_day, default_master_id, description, is_active,
sort_order. Gated by ADMIN_SETTINGS_EDIT.
Migration: posts.default_master_id FK → users (nullOnDelete).
Inline rename from calendar: click any post's row label opens a modal
with name field + default master dropdown. Saved values propagate
immediately to next appointment creation.
Auto-fill in new appointment: when creating an appointment via the "+"
cell button on a post row, master_id is pre-filled from
post.default_master_id (if not already set by groupBy='master' row).
== Quick-add bay ==
"+ Pod nou" button in toolbar opens a small modal (no full page nav):
name, color picker, hours/day, description. createPost() saves and
refreshes the row list. Designed for "yard space" use-cases — names
like "Curte 1" or "Atelier electric" are first-class, not workarounds.
== PDF export ==
"🖨 PDF programări" button calls exportPdf() which uses the existing
dompdf integration (already installed). Renders pdf/appointments.blade.php
grouped by day with table per day showing time/title/client+vehicle/
post/master/status. Romanian date headers ("Marți, 10 Iunie 2026").
streamDownload with filename programari_YYYY-MM-DD_YYYY-MM-DD.pdf.
== List view ==
getListAppointments() returns flat array of all appointments in the
visible period (date-range respects current viewMode), with full
client/vehicle/post/master joined. Status filter respected. Row click
opens the existing event detail panel.
== Tests ==
CalendarEnhancementsTest (8):
- viewMode='day' returns 1 day
- viewMode='month' returns 30 days for June 2026
- viewMode='custom' uses customStart..customEnd range
- quick-add post via Livewire createPost persists with all fields
- rename post updates name + default_master_id
- new appointment auto-fills master_id from post's default_master_id
- list view returns flat array with phone + post name joined
- exportPdf returns StreamedResponse with .pdf filename
Suite: 285 passed (802 assertions). Was 277.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the remaining ~4% from CONFORMITY-12-15.md. All four modules at
or near 100% conformance after this commit.
== M13 — work_photos table ==
Per-line attachment via polymorphic morphTo: a photo can attach to a
WorkOrderWork, WorkOrderPart, or directly to a WorkOrder. Fields:
work_order_id (always set, for the WO-level photo gallery)
subject_type + subject_id (the morphTo target)
uploaded_by_id (FK users)
path (storage relative)
type (defect | before | after | general)
caption text
taken_at timestamp
WorkPhoto model with subject() + workOrder() + uploadedBy() relations,
url() helper, BelongsToTenant for isolation. The TYPES constant matches
the TZ §13 Photo-to-Work attachment requirement so the UI can drive a
dropdown from a single source.
== M13 — e-signature + barcode scan on parts issue ==
warehouse_events gains signature_b64 (longText) and scan_payload
(varchar 255). Both nullable — every existing issue/return event stays
valid.
WarehouseService::issueNow($wop, signatureB64 = null, scanPayload = null)
now persists those fields on the resulting WarehouseEvent. Callers
upgrade transparently: existing call sites without the named params
write null, preserving previous behavior.
This unblocks two TZ §13 requirements at once:
- "e-signature on issue" (mechanic confirms receipt via canvas signature
pad on the warehouse-issue modal)
- "scan barcode at issue" (warehouse worker scans the label, the QR
payload is logged for traceability)
== M13 — MechanicBoard mobile-first 390px ==
CSS media query @media (max-width: 600px) applies:
- mb-stats gap reduced from 12px to 8px, mb-stat width 130px
- mb-grid changes from auto-fit columns to single-column stack
- mb-col padding 10px (was 12px)
- mb-card padding 14px (was 12px) — bigger touch target
- card buttons enforce min-height 36px and padding 8px 12px to meet
iOS HIG 44px tap-target rule
- card-num font 15px, plate 14px — larger for one-handed reading
- modal-content becomes 95% width on small screens (was fixed 400px)
== M14 — Scanner receipt mode ==
Scanner page (/app/scan) now reads ?purchase=N from query string. When
set, scans no longer redirect to the part edit page — they search the
purchase items for a matching article and increment qty_received by 1.
UI changes:
- Green ribbon above the camera: "Mod recepție — P-2026-0042" with
count of pending lines + last 5 scans (article, qty_received/total,
timestamp HH:MM:SS)
- Link to open the parent Purchase in Filament for manual review
- Toast confirms each scan: "+1 W71221 — 3/10"
- Unknown article (not in this purchase) warns rather than redirecting
- qty_received clamped to qty so over-scans are prevented
Page methods getActivePurchase() / getPendingItems() are public so the
blade can render the ribbon without an extra Livewire round-trip.
== Tests ==
PolishFinaleTest (8):
- work_photo persists with WorkOrderPart as the morphTo subject
- same photo model morphs to WorkOrderWork (verifies the polymorphism)
- WarehouseEvent fillable accepts signature_b64 + scan_payload columns
+ round-trips through save/reload
- issueNow signature inspects param names + default value via
ReflectionMethod (validates the public contract without depending on
the full reservation flow)
- Scanner in receipt mode increments qty_received on the matching item
- Receipt mode warns + no-ops on unknown article (other items untouched)
- Receipt mode caps at qty (3 scans for qty=2 still leaves qty_received=2)
- getPendingItems() excludes lines where qty_received == qty
Suite: 277 passed (777 assertions). Was 269.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>