4 Commits

Author SHA1 Message Date
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 03e030d6d2 feat: tier 3 polish — M12/13/14/15 deep cleanup
Closes the remaining ~50h of items from CONFORMITY-12-15.md across all
four modules. Single umbrella migration (2026_06_05_000004) lands four
tables + 5 column additions, no downtime risk.

== M12 — body_type + transmission + pricing audit log ==

Vehicle gains body_type (12 values: sedan/hatchback/suv/crossover/pickup/
van/truck/coupe/wagon/convertible/minivan/moto) and transmission_type
(6 values: manual/automatic/cvt/dsg/dct/amt). These are separate from
vehicle_class so admin can configure DSG-only coefficients without
contaminating the SUV detection.

PricingCoefficient.matches() now also tests:
  - conditions.body_types[] against ctx.body_type
  - conditions.transmissions[] against ctx.transmission

PricingEngine builds the richer ctx and exposes it on the quote return
under quote.context.

New pricing_application_logs table (append-only) — call
PricingEngine::logApplication($quote, $subject, $vehicle, $client, $part)
after applying a price to a WO line. Stores base, final, full
applied[] array, and the ctx snapshot so the question "why was this
priced at 218 lei in March?" stays answerable forever.

PricingCoefficientResource form gains CheckboxList for body_types and
transmissions (3-column layouts, full-width). Both are optional —
empty list = applies to anything.

== M13 — Mechanic REST API + KPI ==

New MechanicApiController with 7 endpoints under /api/v1/mechanic/:
  GET    /board               — own non-done WOs with their works expanded
  GET    /kpi?period=YYYY-MM  — own aggregates for the period
  POST   /tasks/{w}/start
  POST   /tasks/{w}/pause
  POST   /tasks/{w}/resume
  POST   /tasks/{w}/done
  POST   /tasks/{w}/block     — validates reason from BLOCK_REASONS enum

Every endpoint authorizes ownership: $work->workOrder->master_id ===
auth()->id() else 403. board() returns null pending_works so native
apps don't make round-trips. workPayload() emits efficiency_pct and
efficiency_class on every response.

New MechanicKpi Filament page at /app/mechanic-kpi (Service group). Same
aggregation logic but tenant-wide: groups WorkOrderWork rows by
master_id for the selected period, computes totals + efficiency_pct +
revenue. Period navigation via ◀/▶ buttons, default = current month.
Color-coded efficiency badges (green ≤100%, amber ≤130%, red >130%).
Rows sort by revenue descending — easy "top earners this month" view.

== M14 — OCR async via Laravel queue ==

New ocr_jobs table (id, supplier_id?, source_type, file_path, status,
result JSON, error_message, ai_provider, tokens_used, purchase_id?,
processed_at). Idempotent migration.

New OcrJob model + ProcessOcrJob queueable job. Job re-establishes
tenant context inside the worker (Company::find + TenantManager::setCurrent)
since queue workers don't inherit middleware-resolved tenants.

handle() walks: status=pending → processing, calls OcrInvoiceService::extract,
on success → status=done + result + ai_provider; on throw → status=failed
+ error_message. Failed jobs auto-retry once (tries=2) with 120s timeout.

The existing synchronous OcrInvoiceService stays for inline use cases
(tests, quick imports). The job is now the canonical path for the
admin UI to keep requests sub-100ms.

== M15 — eta_promised + JSON tracking + notifications log ==

Three new wo columns: eta_promised (initial commitment, never changes),
eta_change_reason (text for "așteptăm piesă"), eta_updated_at (when
the current eta was last touched). Existing eta_at remains as "current"
ETA so the UI can render both side-by-side.

New /api/track/{token} JSON endpoint (public, tenant-scoped via subdomain):
  number, status, status_label, progress %, client, vehicle, plate, master,
  eta_promised, eta_current, eta_change_reason, total, pay_status,
  pending_approvals[] (each with kind/id/name/amount/approve_url —
  signed URLs ready for native app webview),
  timeline[] (from activity_log, last 20 events).

NotificationDispatcher::dispatch() gains optional workOrderId param.
Every send call (success or failure) now writes one row to the new
client_notifications_log table with channel/template_key/status (sent
or failed)/error_detail/sent_at. Failures of logging are swallowed
so a missing activity_log never breaks notifications. workOrderReady
and paymentReceived pass the WO id through; others can be wired in
future commits without schema change.

New tables tracked:
  client_notifications_log — every push to client, append-only
  pricing_application_logs — every pricing decision, append-only
  ocr_jobs — async OCR job queue

== Tests ==

PolishTier3Test (11):
- M12: body_type condition match/no-match; transmission DSG match;
  pricing_log row persists base/final/applied/ctx
- M13: mechanic API board scoped to own WOs; start task on foreign
  work returns 403; KPI endpoint computes 2.5/3 = 83% efficiency
  across 2 done works in period
- M14: ocr_job queueable + Queue::fake assertion
- M15: tracking JSON returns ETA promised/current/reason + pending
  approvals with correctly-signed approve_url; dispatcher writes
  ClientNotificationLog row on workOrderReady
- M12: vehicle body_type + transmission_type round-trip through save

Suite: 269 passed (761 assertions). Was 258.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 05:31:50 +00:00
Vasyka 0e3119a6e2 feat: M14 Excel import wizard + M15 client approval via tracking link
Top-ROI items from CONFORMITY-12-15.md. Together: ~40h of TZ work
delivered in one pass.

== M14 — Excel/CSV invoice import wizard ==

phpoffice/phpspreadsheet ^5.7 added as composer dep — parses both XLSX
and CSV cleanly.

ExcelInvoiceImportService (app/Services/ExcelInvoiceImportService.php):
- headersPreview($path)          → first 5 rows + detected column letters
- preview($path, $mapping)       → all rows classified as found/new/no_article
- import($supplier, $rows, $createNew=true) → creates Purchase + items,
                                    auto-creates Parts for "new" rows
- rememberMapping / rememberedMappingFor($supplier) — round-trips JSON
  config (article_col / name_col / qty_col / price_col / brand_col? /
  header_row / sheet_name?) per supplier so the second import is
  instant

Decimal parser tolerates European formats: "1 234,56", "1,234.56",
non-breaking spaces (U+00A0 NBSP common in copy-pastes from PDF).
Article matching uses single batch query (Part::whereIn) — O(1) for
the whole sheet, not O(rows).

ExcelImportWizard Filament page (/app/excel-import-wizard) — 4-step
Livewire wizard:
  1. Upload + supplier select (saved mapping auto-loads if exists)
  2. Column mapping with first-3-rows preview table + per-column
     dropdowns
  3. Preview with status badges per row ( Found / ⚠️ New /  Missing)
     + summary counts
  4. Confirmation → "Open Purchase" CTA

Stored in nav group "Stoc & Finanțe", sort 65. Width Full.

Migration: supplier_invoice_mappings (id, company_id, supplier_id UNIQUE,
mapping_config JSON, sample_file_name, last_used_at, timestamps).
Per-tenant scope via BelongsToTenant.

== M15 — Client approval via tracking link (the P0 from TZ §15) ==

Migration: adds 4 columns to wo_works AND wo_parts:
- requires_approval boolean default false
- approved_at timestamp nullable
- approval_token varchar(32) nullable (indexed for fast lookup)
- declined_at timestamp nullable

Both model booted hooks: when a row is saved with requires_approval=true
and no token yet, auto-generate Str::random(24). Models gain
isPendingApproval() helper returning true only while not yet approved
nor declined.

Public route: POST /t/{token}/approve/{kind}/{lineToken}
  kind = 'work' | 'part'
  body: decision = 'approve' | 'decline'
The line's approval_token IS the credential — anyone with the URL can
act. No CSRF token required since this is the unauthed public tracking
flow (the tracking_token + line approval_token combo functions as
shared-secret). Form-encoded POST with csrf_field() on the public form
keeps Laravel happy.

TrackingController::show() now eager-loads works + parts, computes
pendingWorks and pendingParts collections, passes them to the view.
TrackingController::approve() validates kind, locates the line by
(work_order_id, approval_token), idempotently marks approved_at or
declined_at, redirects back to /t/{token} with a flash status.

UI banner (tracking/show.blade.php) at the top of the page:
- Amber warning "⚠ Necesită aprobarea ta"
- Per-line card: title + amount (ore/qty + total MDL) + two buttons
  (green Aprob / outline-red Nu aprob)
- Disappears as soon as approved/declined
- Success/error flash above the banner after each action

== Tests ==

ExcelInvoiceImportTest (5):
- headers_preview returns first 5 rows + column letters
- preview classifies rows as found/new/no_article based on Part DB
- import creates Purchase with items + auto-creates parts for "new"
- remember_mapping upserts, no duplicate per supplier
- decimal parser tolerates "1 234,56" European format with NBSP

TrackingApprovalTest (7):
- creating a work with requires_approval auto-generates 24-char token
- POST /t/{token}/approve/work/{lineToken} marks approved_at
- POST with decision=decline marks declined_at instead
- wrong line token redirects with error flash (no leak)
- already-approved line cannot be approved again (idempotent)
- tracking page renders "Necesită aprobarea ta" banner when pending
- approved line vanishes from banner on next page load

Suite: 246 passed (700 assertions). Was 234.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 04:56:06 +00:00
Vasyka edcdba9d53 Stage 3 — WO photos + ETA + QR + public tracking page
- HasMedia (Spatie) on WorkOrder with `photos` collection
- eta_at + tracking_token columns; token auto-generated on create
- Public /t/{token} page — tenant-scoped via subdomain, white-label themed
- QR code SVG via chillerlan/php-qrcode (inline modal + download)
- Filament: SpatieMediaLibraryFileUpload + ETA picker + tracking section
- EditWorkOrder header action "Link client (QR)" modal
- Fix: Auditable::dontSubmitEmptyLogs() → dontLogEmptyChanges() (removed in activitylog)
- Tests: TrackingPageTest (4 pass) covering token gen + cross-tenant isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:21:23 +00:00