Commit Graph

47 Commits

Author SHA1 Message Date
Vasyka fa8704f8b6 feat(work-orders): new Mitchell1-style dashboard view (Phase 1 — layout + tabs)
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>
2026-08-05 18:48:52 +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 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 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 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 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 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 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 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 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
Vasyka 80c3834263 feat: calendar enhancements — view modes, post CRUD, PDF, list
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>
2026-06-06 07:34:27 +00:00
Vasyka 2c66547967 feat: polish finale — work_photos + e-signature + mobile + scan receipt
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>
2026-06-06 07:24:15 +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 cbcf08b28c feat: tier 2 — M12 Pricing UI + M13 mechanic granular workflow
Closes "Important pentru produs complet" tier from CONFORMITY-12-15.md.

== M12 — PricingCoefficientResource + breakdown ==

New Filament resource at /app/pricing-coefficients (Admin group, gated
by ADMIN_SETTINGS_EDIT). Replaces the previous "edit JSON in DB"
configuration workflow.

Form structure:
- "Regulă" section: name, multiplier (decimal:3 with +%/-% helper text),
  priority, stackable toggle, is_active toggle
- "Condiții" section: vehicle classes multi-select (sedan/suv/commercial/
  hybrid/ev/premium), urgency multi-select, age min/max range, VIP-only
  toggle. Empty = applies to everything.

Table columns:
- name (searchable + sortable)
- multiplier formatted as +15% / -10% / 0% with color coding (green for
  positive, red for negative)
- conditions summarized: "Tip: SUV / Vârstă 10-99 ani / Urgență: Express"
- stackable boolean icon
- priority
- is_active inline toggle column (no edit needed)

For the breakdown display: the existing "Preț inteligent" action on WO
part lines (PartsRelationManager) already opens a modal that renders
`filament/tenant/smart-price.blade.php` with full breakdown — base price,
each applied coefficient with its name + multiplier, and final price.
Confirmed working; no change needed.

== M13 — Mechanic granular state machine ==

Migration adds 8 columns to wo_works:
- mechanic_status enum: pending | in_progress | paused | done | blocked
- mechanic_started_at, mechanic_done_at timestamps
- actual_hours decimal(6,2)
- paused_seconds_total integer (cumulative across multiple pauses)
- paused_at timestamp (when current pause started)
- block_reason enum: missing_part | awaiting_approval | broken_equipment | other
- block_note text (free-form context)

Model defaults via $attributes so create() with no mechanic_status
yields 'pending' (not null).

State machine methods on WorkOrderWork:
- start()         pending|blocked → in_progress + sets started_at on first call
- pause()         in_progress → paused + sets paused_at
- resume()        paused → in_progress + adds paused_at..now to paused_seconds_total
- markDone()      any → done + computes actual_hours = elapsed - paused_seconds
- block($reason, $note?)  any → blocked + persists reason/note (invalid reasons ignored)

Transitions enforce preconditions silently (no exceptions) — calling
pause() on a pending work is a no-op, which keeps the UI buttons simple.

Efficiency indicator:
- efficiencyClass(): 'green' (actual <= norm), 'amber' (1.0 < ratio <= 1.3),
  'red' (ratio > 1.3). null when no actual data.
- efficiencyPct(): integer percentage of norm time used.

== MechanicBoard UI ==

Each WO card now expands to show its WorkOrderWork lines with:
- Title + colored status badge per mechanic_status (gray/blue/amber/green/red)
- Norm hours displayed next to actual hours when known
- Efficiency percentage colored by class
- "⚠ {reason}" line when blocked
- Context-aware transition buttons:
  - pending|blocked: [▶ Start]
  - in_progress:    [⏸ Pauză] [✓ Done] [🔴 Blochez]
  - paused:         [▶ Reia]  [✓ Done]
  - done: none (terminal)

Block button opens an inline modal (vanilla CSS, no third-party):
- Select with 4 reasons (lipsă piesă / aștept aprobare / echipament defect / altă problemă)
- Free-form textarea for details
- Confirm/Cancel buttons
- Backdrop click cancels

Safety: every transition + block action verifies $work->workOrder->master_id
matches auth()->id() before mutating. Other mechanics' works are silently
refused (no error leak).

== Tests ==

MechanicWorkflowTest (8):
- start sets mechanic_status=in_progress + started_at
- pause→resume accumulates paused_seconds_total correctly
- markDone computes actual_hours = elapsed - paused, with mid-task pause
- block persists reason + note
- efficiency thresholds: green (<= norm), amber (1.0-1.3x), red (>1.3x), null
- invalid block_reason is silently ignored
- mechanic board startWork refuses other mechanic's work (auth gate)
- confirmBlock modal flow round-trip via Livewire

PricingCoefficientResourceTest (4):
- admin canViewAny → true
- mechanic canViewAny → false (via direct canDo to bypass actingAs team-context fragility)
- coefficient creation + matches(context) round-trip
- PricingEngine.quote() returns both stackable and non-stackable coefficients
  with correct names in the applied[] array

Suite: 258 passed (728 assertions). Was 246.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 05:15:01 +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 1d5ea6d261 feat: Calendar Vizual v2 (Pod×Days matrix) + hidden markup
Implements 2 of the biggest items from /tmp/service/new docs:

== Calendar Vizual v2 (from 02-prototip-calendar-vizual.html) ==

Replaces the FullCalendar week view (the one that visually collapsed after
Livewire re-renders) with a server-rendered matrix that the harness
already drives through Livewire — no third-party JS to clash with Filament.

Layout: 8-column CSS grid (1 row-label + 7 days). Rows are either Posts
(Pod 1, Pod 2…) or active masters depending on toolbar switch. Each
cell holds 0..N event cards.

Per-cell load badge (top-right):
  hours_planned / capacity  →  badge color (gray <50%, orange 50–90%, red ≥90%)

Drag-drop: HTML5 native, Alpine.js holds the dragEventId, moveEvent($id,
$toRowId, $toDate) in PHP updates either post_id or master_id (depending
on groupBy mode) plus date — works seamlessly when re-grouping.

KPI bar (4 cards above toolbar):
- Ore programate X / Y · % capacity
- Fișe deschise (orange)
- Confirmate X/Y (green) + confirmation rate
- No-show alert (red) — scheduled events <24h away that are still unconfirmed

Toolbar:
- ◀ Week ▶ + Astăzi (reset)
- Date label "01 — 07 iunie 2026"
- Grupare switch: Pod ↔ Mecanic
- Filtru: master dropdown + status dropdown (Confirmate/Neconfirmate/În lucru)

Today column highlighted blue; Sunday column hatched as closed
(non-interactive, no drop target); Saturday muted as weekend.

Event card color = master.color (deterministic, matches profile setting),
shown as left border + background tint. Title = client name; meta =
"VW Passat · CIU 001"; time = "08:00–12:00 · V.".

Click empty cell → quick-create panel (right slide-in) with date+pod
pre-filled. Click event → detail panel with Client/Phone/Auto/Plate/
Master/Pod + delete + edit.

Legend section at bottom (mecanici dots, load colors, day states).

== Hidden Markup (from gap-analysis.md #3) ==

Adds `hidden_markup_pct` decimal to parts. Customer documents continue
to show the standard sell_price; the hidden markup is an internal margin
indicator used for B2B contracts and corporate analytics.

Part::internalCostWithHiddenMarkup() returns buy_price * (1 + pct/100).
Falls back to buy_price when pct is null. Decimal:2 cast so persistence
round-trips cleanly.

== Schema migration ==

Idempotent (hasColumn guards):
- posts.hours_per_day decimal(5,1) default 10
- posts.description varchar(255) nullable
- parts.hidden_markup_pct decimal(5,2) nullable

== Tests ==

+11 new in CalendarBoardV2Test (8) + HiddenMarkupTest (3):
- get_days returns 7 days with today flagged + Sunday closed + Saturday weekend
- get_rows returns posts when grouped by post + with capacity
- get_rows returns masters when grouped by master + Fără maistru fallback row
- matrix places events in correct cells + sums hours
- move_event reassigns post_id and date
- create_appt inserts appointment via panel form
- stats compute utilization from events (8h / 60h capacity = 13%)
- status filter narrows to confirmed only
- hidden_markup applies pct correctly + falls back to buy_price + persists

Suite: 196 passed (551 assertions). Was 185.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 21:50:22 +00:00
Vasyka d9b198a235 feat: Pipeline board matches mockup pixel-by-pixel
Audit pass against /tmp/service/todo/psauto-pipeline-redesign.html — 10
gaps closed.

1. In-page TOPBAR (mockup had it; was missing): "Pipeline" title,
   sep, search box "Caută client, mașină, număr...", and right-side
   Filtre / Export / + Deal nou (primary) buttons. Search input is
   wire:model.live.debounce 300ms.

2. SEARCH actually filters cards: $searchQuery property in
   PipelineBoard scans subject + client_name + plate + code + phone
   across all 6 columns, case-insensitive.

3. "+ Deal nou" + "+ Adaugă cerere" (per-column bottom) now open the
   SAME right-side panel in "new form" mode. Inline create form:
   Nume / Telefon / Auto / Sursă / Notițe → createNewLead() inserts
   Lead with status=new, lands in col 1 instantly without leaving page.
   Validation: name + phone required.

4. EXPORT button calls exportCsv() — streams a CSV of current filtered
   columns (etapă, cod, subiect, client, telefon, auto, sumă,
   responsabil, stare timp).

5. PERIOD selector chip shows current month in Romanian
   (now()->locale('ro')->isoFormat('MMMM YYYY')) — matches "Iunie 2026".

6. HOVER icons now match mockup exactly per column:
   - request: 📅 schedule / 📞 phone / ⋮ edit
   - quote:   📅 schedule / 💬 wa / ⋮ edit
   - scheduled: 📄 file-plus (start WO) / 💬 wa / ⋮ edit
   - in_work: 👁 eye (open WO) / 💬 wa / ✓ mark Gata
   - ready:   💰 cash (mark paid) / 📞 phone / ⋮ edit
   - paid:    NONE (col 6 has no hover actions per mockup)

7. Col 6 "Achitat azi" cards now opacity:0.65, no hover actions,
   no time line, no assignee name (just avatar) — exactly as in mockup.

8. Sum display: amount == 0 renders "—" instead of "0 MDL", both in
   card footer and list view.

9. "Avans achitat" tag (blue) appears on Ready cards with partial
   payment (pay_status='partial'); "Neachitat" amber only when fully
   unpaid. Matches mockup col 5 example "Nissan Qashqai · Gata +
   Avans achitat".

10. Link tracking quick-action: appears in detail panel "Acțiuni rapide"
    grid when WO has tracking_url. Sits alongside WhatsApp / Sună / SMS.

Two-panel architecture: $showNewForm and $openCardKey are mutually
exclusive. Click outside or ✕ closes the panel; opening one closes
the other.

Tests: +4 (createNewLead happy path, validation, search filter,
partial payment tag). Suite 185/185 (was 181).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 21:18:54 +00:00
Vasyka 3c0f3ba39e feat: Pipeline board full-bleed + hover actions + Programare→Calendar
Addresses the gaps surfaced after the first redesign — the board was still
boxed in Filament chrome (not truly full-page), hover floating-actions and
"+ Adaugă" CTAs were missing, and the P0 "Programează" from deal card had
no calendar wiring.

Full-page:
- getMaxContentWidth() = Width::Full
- getHeading()/getSubheading() return empty so Filament's title bar
  disappears, leaving the kanban edge-to-edge
- CSS uses :has(.pb-shell) to strip Filament's page padding + heading
  block at the layout level
- Board height = calc(100vh - 64px); columns scroll independently

Hover floating-actions on every card (column-aware):
- Cols 1-2 (Cerere / Calculație): 📅 quickSchedule
- Col 3 (Programat): ▶ start work (creates WO)
- Col 4 (În lucru): ✓ mark Gata
- Col 5 (Gata): 💰 mark Achitat
- All cards with phone: 📞 tel: + 💬 wa.me
- All cards: ↗ open in resource edit
- Shown only on .pb-deal:hover, positioned absolute top-right

"+ Adaugă" CTA at column bottom:
- Cols 1-3 → /app/leads/create
- Cols 4-5 → /app/work-orders/create

Programare → Calendar (P0 AAA):
- quickSchedule($key) on PipelineBoard creates a real Appointment row for
  tomorrow 10:00 linked to (client_id, vehicle_id, master_id, deal_id),
  sets deal.stage='scheduled' + scheduled_at, then shows a toast
- Panel bottom action bar gains "📅 Programează" CTA for lead/deal cards
- "📅 Calendar" jump CTA for WO cards
- calendarUrl() returns the canonical filament.tenant.pages.calendar-board
  route

Empty column state now reads "Gol — trage un card aici" instead of just
"Gol" so the drop affordance is explicit.

Stat strip + filter bar sticky at top; board fills the remaining viewport.

Tests: +1 (quickSchedule creates Appointment + moves deal). Suite 181/181.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:14:20 +00:00
Vasyka 3603c0e43b feat: rich Pipeline board — unified Lead/Deal/WO Kanban with SLA + drag-drop transitions
Replaces the bare 6-status WO Kanban with the unified Pipeline view from
/tmp/service/todo/psauto-pipeline-redesign.html. Six columns now span the
entire customer journey end-to-end:

  Cerere nouă → Calculație → Programat → În lucru → Gata → Achitat azi
  └─ Lead/Deal  └─ Deal      └─ Deal     └─ WO       └─ WO    └─ WO+Payment

Cross-model drag-drop transitions:
- Lead → Calculație: Lead::convert() creates Deal at stage=contact, marks
  quote_sent_at = now, quote_status = sent
- Deal (any earlier stage) → În lucru: spawns a WorkOrder from the deal
  (client, vehicle, master, total, complaint), sets deal.stage=in_work,
  links wo.deal_id
- WO → Gata: status=ready + fires NotificationDispatcher::workOrderReady
  so client gets Telegram/email automatically
- WO → Achitat: creates Payment for remaining balance + status=done,
  closed_at=today (pay_status syncs to paid via Payment booted hook)

Rich card content per the mockup:
- Red urgent stripe (left border) for Deal.urgent or WO.urgency!=normal
- Source tag (Instagram/Site/Apel/etc.) on lead/deal cards
- Quote status badge ("Trimis · fără răspuns" amber / "Văzut ✓" blue /
  "A răspuns" green) based on deal.quote_status
- Scheduled time + bay tag ("05.06 · 09:00" + "Post 2")
- Fișă FL-NNN purple tag on WO cards
- "Necesită aprobare" amber tag when wo.status=agreement
- Progress bar (purple, 0-100%) on in-work cards: works_done + parts_installed
  over total lines
- SLA time line per card with overdue red color:
  * Lead 60+ min not contacted = overdue
  * Quote 2h+ no response = overdue
  * Ready 30+ min not paid = overdue (with phone icon)
  * WO past ETA = overdue
- Assignee avatar (deterministic CRC32 color: blue/green/purple/amber)
- Amount in MDL, formatted

Stat strip (6 metrics computed live):
- Total deals active (sum of cols 1-5)
- MDL pipeline total
- MDL closed today (Payment sum where paid_at=today)
- Necesită acțiune (overdue + urgent + pending approval)
- Rata conversie 30d (won / (won+lost) %)
- Depășit termen (count WO past eta_at)

Filter chips wire-driven: Toate / Ale mele (assigned_to=me) /
Urgente (urgent=true OR wo.urgency!=normal) / Azi.

View toggle: Kanban ↔ Listă (table with all cards flat, sortable by stage).

Slide-in detail panel:
- 6-step stage stepper highlighting current
- Client / Telefon (blue clickable) / Auto / Sursă / Responsabil / Sumă /
  De achitat (live computed balanceDue for WOs)
- Note / Reclamație
- Linked Fișă card with status badge, progress, ETA, "necesită aprobare"
  alert + tracking link
- Activity timeline from Spatie activity-log
- Quick actions: WhatsApp (wa.me/<phone>), Sună (tel:), SMS (sms:),
  Deschide (jumps to Filament resource edit)

DealResource hidden from nav (shouldRegisterNavigation=false) since
PipelineBoard is the canonical entry, but its edit/create routes stay
intact — the panel deep-links to them.

Auto-refresh: wire:poll.10s keeps the board live without WebSocket
dependency. Drag-drop is HTML5 native + Livewire wire:click for ops.

Dark mode supported via CSS variables overridden in .dark scope.

Migration: extend deals table with urgent, quote_sent_at, quote_status,
quote_seen_at, scheduled_at, bay, confirmed_at, confirmed_via,
last_action_at. Idempotent (hasColumn guards). Deal model auto-updates
last_action_at on saving.

Tests: 7 new + full suite 180/180 green (was 173).
- partition leads/deals/wos by column
- stats computation: active, pipeline_mdl, closed_today_mdl
- lead→quote transition converts lead into deal
- deal→in_work creates WorkOrder linked back to deal
- wo→paid creates payment for balance + marks done
- filter "mine" narrows to assigned user
- openCard loads panel detail with correct stepper position

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:02:44 +00:00
Vasyka 0e3f9e8bca feat: AI model selector + i18n nav labels (RU/EN) on new modules
AI model selector:
- AiAssistantService::MODEL_DEFAULTS and MODEL_OPTIONS const tables (3 picks per
  provider: Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, OpenAI 4o / 4o-mini,
  Gemini 1.5 Pro / Flash). Default upgraded from Sonnet 4.5 → Sonnet 4.6.
- modelFor(provider, company?) resolves tenant override > global default.
- All 8 hardcoded model strings replaced with modelFor() across callClaude
  (chat with tool-use), callOpenAI, callGemini (chat), postClaude/postOpenAI/
  postGemini (single-shot), and OcrInvoiceService.
- Settings page adds 3 model selectors per provider with persistence at
  settings.ai.models.{claude,gpt,gemini}.

i18n nav labels:
- TireSet / Bodyshop / Subcontractor / SubcontractJob / PricingCoefficient /
  ShopCustomer resources: getNavigationLabel / getNavigationGroup /
  getModelLabel / getPluralModelLabel return __()-wrapped strings.
- 20 keys added to lang/ru.json and lang/en.json.

Tests (4 new): default model, tenant override wins, unknown provider falls
back to claude default, options dictionary contains each default key.

Full suite: 134 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 06:23:21 +00:00
Vasyka 954ba8f059 Stage 12 — Online Store: public catalog + cart + orders
Schema:
- online_orders (token-tracked, status workflow, delivery method/fee)
- online_order_items (price snapshot, fulfilled flag)
- part_cross_refs (OEM/equivalent codes for search)
- parts.is_published (shop visibility)

Storefront (ShopController, tenant subdomain, /shop):
- Catalog with search across name/article/brand/cross-refs, category +
  in-stock filters, live stock, white-label themed layout
- Part detail page with cross-ref codes
- VIN search → VinDecoder → guided catalog search
- Session cart (per-tenant key), guest checkout, order confirmation page
- Respects settings.shop.enabled (404 when off); tenant-guarded

Part::searchPublished matches cross-ref articles via whereHas.

Order notifications (ShopOrderNotifier, best-effort):
- Staff: Web Push to active users
- Customer: Telegram if phone matches a linked client

Filament (tenant):
- OnlineOrderResource under "Magazin" nav group, status workflow,
  items relation, "Onorează" action issues stock via WarehouseService (FIFO)
- PartResource: is_published toggle + column + bulk publish/unpublish +
  CrossRefsRelationManager
- Settings: shop section (enable, delivery methods, fee, free-over)
- Landing page: shop button when enabled

Tests (6 new):
- catalog 404 when disabled; lists published only; cross-ref search;
  order placement (token + items + total); fulfill issues stock;
  cross-tenant token isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 05:27:51 +00:00
Vasyka c413004930 Stage 15 — PWA complete: install prompt + Web Push notifications
Dependency:
- minishlink/web-push v10 (VAPID JWT + aes128gcm payload encryption)
- Dockerfile: add curl, mbstring, gmp extensions (web-push needs ext-curl)

VAPID:
- config/webpush.php from env; `php artisan push:vapid` generates keypair
- Shared platform keypair; .env.example has empty placeholders

Schema:
- push_subscriptions (user/company, endpoint unique, p256dh, auth, encoding)

WebPushService:
- send / sendToUser / dispatch via WebPush::flush
- Auto-prunes subscriptions reported expired (404/410)

Subscribe flow:
- POST /push/subscribe + /push/unsubscribe (auth, tenant)
- Tenant panel JS subscribes after SW registration with VAPID public key

Service worker (/sw.js):
- Cache v2, push listener → showNotification, notificationclick → focus/open

Install prompt:
- Floating "Instalează aplicația" button wired to beforeinstallprompt

Staff push:
- WorkOrder master_id change → push to assigned mechanic
- Settings "Test notificare push" action

Tests (6 new):
- subscribe stores + upserts; requires auth (401); validation (422);
  service configured; sendToUser with no subs returns zero

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 05:11:18 +00:00
Vasyka e48ef1b755 Stage 7+14 — Mechanic Board + Scan Center
Mechanic Workflow (Stage 7):
- /app/mechanic Filament page filtered to master_id = auth user
- Kanban 4 columns (in_work / awaiting_parts / ready / recent), each card
  shows WO#, plate, client, complaint summary, photo presence
- 2 KPI tiles (active now / closed today)
- Mobile-responsive grid (auto-fit, minmax 260px)

WarehouseService:
- issueNow(WorkOrderPart) — consume reservations immediately scoped to one
  line, without closing the WO (mechanic physically takes part now)
- returnPart(WorkOrderPart, qty?, notes?) — refund to stock as new batch
  at original buy_price, writes `return` event, capped at consumed total

WO PartsRelationManager:
- "Eliberează" action — visible when active reservation exists
- "Restituire" action — visible when consumed reservation exists, with qty
  modal + notes

Scan Center (Stage 14):
- PartResource "QR" action — per-part SVG QR with payload PART:<article|id>
- BulkAction "Tipărește etichete QR" → /parts/labels?ids=N,M (HTML A4 sheet,
  3-col grid, print CSS hides toolbar)
- /app/scan Filament page using html5-qrcode 2.3.8 (CDN), auto-picks back
  camera, decodes → Livewire dispatches scanner-decoded → resolveAndRedirect
- Lookup matches PART:N prefix, parts.article, parts.barcode, or numeric id
- Manual input fallback for browsers without camera

Tests (6 new):
- WarehouseIssueReturnTest (3): issueNow consumes immediately; returnPart
  creates positive batch + return event; over-return is capped
- ScannerLookupTest (3): PART: prefix lookup, raw barcode lookup, unknown miss

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:39:39 +00:00
Vasyka 1ff888131f Stage 16 — AI Layer: VIN decoder + diagnostic / parts / price helpers
VinDecoder (deterministic, no API):
- ISO 3779/3780 parsing: WMI manufacturer (~60 brands), year (cyclical with
  post-2010 disambiguation via position 7), region, plant, NA checksum
- Strip non-VIN chars, accept dashes/spaces, reject I/O/Q per spec

AiAssistantService:
- Refactored provider HTTP into postClaude/postOpenAI/postGemini so both
  chat history and one-shot calls share the same transport
- singleShot(system, userPrompt, provider?) for fire-and-forget calls
- 4 specialized helpers with tight prompts:
  - suggestDiagnosis(WO) — diagnostician based on complaint + VIN info
  - suggestParts(WO, task) — OEM parts list for an operation
  - suggestPrice(Part) — markup recommendation with justification
  - vinRecommendations(vin, mileage) — scheduled maintenance from decoded VIN
- monthlyUsage() — token spend MTD by provider

Filament:
- VehicleResource: "Decode VIN" + "AI: recomandări" actions
- WorkOrderResource Edit: "AI: sugerează diagnostic" header action
- PartResource: "AI: preț recomandat" action
- Shared views: filament.tenant.ai-reply, filament.tenant.vin-decode
- AiAssistant page shows monthly token usage banner

Tests (13 new):
- 8 VinDecoder unit tests with real VIN samples (Honda 2003, VW 1999, Audi
  2014, Dacia, unknown WMI, lowercase/dashes, forbidden chars)
- 5 AiHelpers feature tests with Http::fake covering all providers + no-key
  fallback + token usage aggregation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:24:09 +00:00
Vasyka 85ef2f6e00 Stage 13 — Notifications: Telegram bot + multi-channel + service reminders
Schema:
- clients.telegram_chat_id (linked via /start contact-share)
- clients.notify_prefs (per-client channel order override)
- service_reminders_sent (dedup ledger for the daily cron)

Telegram (per tenant):
- TelegramService (sendMessage, getMe, setWebhook with auto-generated secret)
- Bot token stored in companies.settings.telegram.bot_token
- Webhook /telegram/webhook/{slug} validates X-Telegram-Bot-Api-Secret-Token,
  matches client by last 9 digits of phone, persists chat_id, replies confirm
- /start prompts share-contact; /stop unlinks chat_id

NotificationDispatcher refactor:
- Multi-channel: telegram first if chat_id + bot configured, then email
- Backwards-compat with legacy boolean notify.{type} flags
- 4 HTML-formatted Telegram messages (wo_ready with tracking link, payment,
  appointment, reminder)

Service reminders:
- `reminders:send` artisan command with --slug / --dry-run
- Policy: vehicles whose last closed WO is older than reminder.after_days
  (default 365). Skips if sent within reminder.cooldown_days (default 30).
- Schedule daily 09:00

Filament UI:
- Settings page: Telegram bot token field + "Test bot" + "Set webhook" actions
- Settings page: reminder_after_days + reminder_cooldown_days inputs
- ClientResource: telegram_chat_id readonly badge

Tests (6 new, all pass):
- webhook links client via shared contact
- webhook rejects wrong secret → 401
- dispatcher uses telegram when chat_id present (Http::fake)
- dispatcher falls back to email otherwise
- dispatcher returns false when no channel available
- reminder cron respects 30-day cooldown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:14:17 +00:00
Vasyka 59409e1b11 fix: calendar — register createForm via getForms() + remove broken FullCalendar CSS link
- Filament v5 multi-form pages need protected getForms() returning form names
- FullCalendar v6 bundles CSS in JS — separate <link> 404'd silently
- Added Russian locale alongside Romanian for i18n switcher
2026-05-08 11:27:04 +00:00
Vasyka 8f324b7b8e Currency dropdown + logo on login/topbar via brandLogo closure
- Onboarding + Settings: Forms\Components\Select for currency
  (MDL/EUR/USD/RON/UAH/RUB) instead of TextInput
- TenantPanel: brandName/brandLogo/favicon as closures resolving from
  tenant. Logo now visible on login page, topbar, sidebar (Filament
  v5 brandLogo handles all 3 spots automatically)
- Favicon falls back to logo if favicon not separately uploaded
- Removed manual SIDEBAR_LOGO_BEFORE injection (replaced by brandLogo)
- Removed manual <link rel=icon> in HEAD_END (replaced by ->favicon)
2026-05-08 07:50:12 +00:00
Vasyka 0399262514 Deploy 3: Onboarding wizard + empty states + docs operationale
- 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
2026-05-07 20:16:03 +00:00
Vasyka eaa05d68c1 Deploy 2: 2FA (App + Email) + REST API + CSV import-export + auto backup
- Filament v5 multiFactorAuthentication enabled on both panels (App + Email)
- HasAppAuthentication + HasEmailAuthentication on User and SuperAdmin
- Migration: app_authentication_secret + recovery_codes + email_authentication_at
- Sanctum REST API: /api/v1/login, /me, clients, vehicles, work-orders
- EnsureTokenMatchesTenant middleware blocks cross-tenant token usage
- CsvImportExport service: clients + vehicles bulk via plain CSV
- Import/Export buttons on Client + Vehicle list pages
- ApiTokens page in tenant panel (generate/revoke + last-used)
- BackupAllTenantsCommand + scheduler (daily 03:00, retain 14 days)
- Background scheduler in entrypoint.sh
2026-05-07 19:25:27 +00:00
Vasyka 6c72fc7db1 Batch 3: Integrări placeholder + Backup tenant
═══ Integrări (Marketing → Integrări) ═══
- /app/integrations Page cu 6 carduri (Telegram/WhatsApp/Google Ads/FB/SMS/Webhook)
- Toggle on/off per integrare; salvare în settings.integrations JSON
- Câmpuri specifice per integrare (token/key/id/secret)
- Banner explicativ: 'placeholder UI — implementare separată'

═══ Backup tenant ═══
- TenantBackupService::export($company) → ZIP cu:
  • data/ (1 JSON per tabel: clients/vehicles/leads/deals/work_orders cu sub-relații/...)
  • media/ (logo + favicon)
  • manifest.json (metadata + counts)
- /app/backup Page cu buton 'Descarcă backup acum'
- Streaming download cu deleteFileAfterSend
- Util pentru: backup local, migrare, audit, GDPR right-to-erasure

Total tenant routes: 104.
Toate cele ~26 module din prototip implementate (sau echivalent funcțional).
2026-05-07 17:36:00 +00:00
Vasyka 4b3201ca1c Batch 2: Workload heatmap + Site PSauto + VIN search
═══ Workload heatmap (Încărcare STO) ═══
- /app/workload custom Page (group Analiză)
- Săptămână (Lu-Du) × posturi → matrice ore programate
- Heatmap colorat: verde→galben→roșu pe ratio capacity (10h/zi)
- Navigare săpt anterior/curent/următor
- Programări fără pod → row '— fără pod —' separat

═══ Site PSauto (landing public) ═══
- / pe tenant subdomain → resources/views/site/landing.blade.php
- Hero cu logo + nume + slogan; gradient theme color
- Servicii (din settings.services) — grid card-uri
- Locație/contact + program lucru standardizat
- Mărci suportate (din settings.cars)
- CTA: phone + email
- Footer cu tenant name + powered by AutoCRM

═══ VIN search ═══
- VinDecoder service: WMI hardcoded (24 producători EU/Asia/USA)
  + year codes (2001-2026) — pure offline, fără API extern
- /app/vin-search Page (group Depozit) cu:
  • Input VIN cu uppercase + monospace
  • Decode → producător/țară/an/serial
  • Match VIN-uri din baza Vehicles
  • Search piese din catalog (live debounce 300ms)
- Rezultatele linkează la editor Vehicle/Part

Total tenant routes: 102.
2026-05-07 17:16:09 +00:00
Vasyka 67da97178d Batch 1: Procentaj + Finanțe consolidat + Recomandări
═══ Procentaj (markup rules) ═══
- markup_rules table cu type (category/brand/range), key, range_from/to, markup_pct, priority
- MarkupRule::bestForPart($part) — rezolvare brand → category → range → 30% default
- MarkupRule::applyToPart($part) — recalc sell_price = buy_price × (1 + pct/100)
- Filament resource sub Depozit cu form dinamic per tip
- Action 'Aplică toate regulile la stoc' — recalc tot catalogul (chunk 100)

═══ Finanțe consolidat ═══
- Custom Page /app/finance cu 4 tab-uri:
  • Overview: încasări/cheltuieli/profit/datorii (4 cards)
  • Cashflow: bar chart per zi (verde=in, roșu=out) + Net total
  • P&L: venituri (manopere + piese) vs costuri (cost piese + cheltuieli pe categorie)
    + profit net + marjă %
  • Balance: active (cash net + datorii + stoc), all-time totals
- Period filter: lună / luna trecută / an / 30 zile

═══ Recomandări ═══
- Custom Page /app/recommendations 4 sectiuni:
  • Clienți pierduți (>6 luni fără WO + are istoric)
  • Mașini km>100k (sugestie revizie)
  • Fișe neplătite (rest > 0)
  • VIP fără contact >30 zile

Total tenant routes: 100.
2026-05-07 15:30:04 +00:00
Vasyka 976c0f03e3 AI Assistant — multi-provider chat (Claude / GPT / Gemini)
Schema:
- ai_chats: company_id, user_id, title, provider; index pe activitate
- ai_messages: role (system/user/assistant), content, meta JSON (tokens, latency, model)

Service AiAssistantService (multi-provider):
- ask($chat, $message): persistă mesajul user, build system prompt cu context
  tenant (statistici clienți/mașini/cereri/datorii), apelează API-ul providerului,
  persistă răspunsul cu meta (tokens, latency)
- callClaude: api.anthropic.com/v1/messages cu claude-sonnet-4-5
- callOpenAI: api.openai.com/v1/chat/completions cu gpt-4o-mini
- callGemini: generativelanguage.googleapis.com cu gemini-1.5-flash
- Try/catch pe toate; eroare devine mesaj asistent fără să crape

System prompt include:
- Numele și orașul companiei
- Statistici curente (clienți, mașini, cereri noi, fișe active, datorii)
- Limita stricta: NU inventează date

Custom Filament Page /app/ai-assistant (group Analiză):
- Sidebar stâng: listă conversații (last 20), buton 'Nouă' + delete cu confirm
- Main: bubble chat (user dreapta albastru, asistent stânga gri)
- Meta jos pe răspuns: provider · latency · tokens
- Empty state friendly cu instrucțiuni configurare
- Loading indicator (3 dots animate) când AI răspunde
- Auto-scroll la mesaj nou
- Enter trimite, Shift+Enter newline
- Auto-titlu chat din primul mesaj user (60 chars)

Settings page extins cu secțiune 'Asistent AI':
- Provider implicit (claude/gpt/gemini)
- 3 chei API (password fields, revealable)
- Key-urile salvate în companies.settings.ai (per tenant, izolat)
2026-05-07 14:50:56 +00:00
Vasyka 09fd0bada2 Faza 2 (din continuare): Email notifications
4 Mailables auto-trigger pe model events:
- WorkOrderReadyMail: la WO.status → 'ready', către client.email
  • Atașat PDF fișa lucru (via WorkOrderPdfService)
  • Total/achitat/rest, recomandări (warning box)
- PaymentReceivedMail: la Payment::created, confirmare cu sumă/metodă/ref
- AppointmentConfirmedMail: la Appointment::created status='scheduled'
- ServiceReminderMail: dispatch manual (vehicle, type=itp/oil/general, note)

Layout email branded (resources/views/emails/layout.blade.php):
- Header cu logo tenant + theme_color border-bottom
- Footer cu telefon/email/disclaimer
- Stiluri inline (compatibil tot mail client)

Settings page extins cu 4 toggle:
- 'Mașina e gata de ridicat'
- 'Confirmare plată primită'
- 'Programare confirmată'
- 'Reminder ITP / revizie'
Salvate în companies.settings.notify (JSON), default true.

NotificationDispatcher service centralizat:
- Verifică isEnabled() pe settings.notify[$key]
- Skip dacă client n-are email
- Try/catch + Log::warning pe eșec (nu crapă request-ul)

Mailables folosesc UsesTenantBranding trait pentru context unitar.
Test prin Mailpit: https://mailpit.service.mir.md (capturează toate).
2026-05-07 13:20:19 +00:00
Vasyka bfe58ed286 Faza 1 (din lista de continuare): Calendar vizual cu FullCalendar 6
- Custom Filament Page CalendarBoard la /app/calendar-board (group CRM)
- FullCalendar 6.1.15 din CDN + locale RO
- View-uri: zi (timeGridDay), săptămână (timeGridWeek default), lună
- Programări colorate per maistru (din User.color)
- Live event loading: Livewire $wire.getEvents(start, end)
- Drag-drop reschedule: eventDrop → $wire.moveEvent(id, start, end)
- Resize event (extinde durată): eventResize
- Click slot gol → quick-create modal cu form Filament populat cu data/timpul
  - Câmpuri: title, time_start/end, client (live), vehicle (filtrat după client),
    master, post, notes
- Click event → confirm + delete
- Toolbar: prev/next/today + month/week/day switch
- 07:00–21:00 grid cu sloturi 30 min, today indicator live
- Modal stilizat (CSS scoped) cu close button + ESC

Total tenant routes: 93.
2026-05-07 13:10:27 +00:00
Vasyka f1d196f018 Faza 7: White-label per tenant — logo + theme color dinamic
- spatie/laravel-medialibrary instalat (migration media table)
- filament/spatie-laravel-media-library-plugin
- Company implements HasMedia + InteractsWithMedia
  - collections: 'logo' + 'favicon' (singleFile)
  - getLogoUrl() / getFaviconUrl() helpers
- Settings page extins: secțiune Logo & favicon cu FileUpload
  - On save: clear collection + addMedia from temp upload + cleanup tmp file
- TenantPanelProvider render hooks:
  - HEAD_END: theme-color meta + favicon + CSS vars override
    (--primary-50 → --primary-950 generate din hex theme_color)
  - SIDEBAR_LOGO_BEFORE: afișare logo upload-uit, max-height 56px

Cum funcționează:
- Tenant uploadează logo în Settings
- La fiecare request, render hook injectează <style> cu CSS vars custom
- Filament respectă --primary-* → toate butoanele/badge-urile primesc culoarea brand
- Logo apare deasupra meniului (sidebar)
2026-05-07 12:51:19 +00:00
Vasyka 06696727dd Faza 6: Activity log + Kanban + Payroll + cleanup
══════ Activity log (Spatie) ══════
- spatie/laravel-activitylog v5 instalat
- Migration cu company_id pentru tenant scoping
- Trait Auditable (App\Models\Concerns\Auditable):
  - LogOptions cu logFillable + logOnlyDirty + dontSubmitEmptyLogs
  - tapActivity auto-fill company_id + causer
  - Descrieri RO (creat/modificat/șters/restaurat)
- Aplicat pe: Client, Vehicle, Lead, Deal, WorkOrder, Payment, Expense
- ActivityResource (group Admin → Jurnal activitate)
  - Listă read-only, scope pe tenant, filtre by description/today

══════ Kanban Work Orders ══════
- Custom Filament page la /app/kanban (group Service)
- 6 coloane (new → diagnosis → agreement → in_work → awaiting_parts → ready)
- Drag-drop nativ HTML5 cu wire:click moveCard()
- Cards arată: număr fișă, client, auto, plate, master, total
- Link 'Deschide' direct la editare WO

══════ Payroll (Salarii) ══════
Schema:
- employee_profiles: user_id, position, base_salary, works_pct, parts_pct
- payroll_runs: period (YYYY-MM), base, works_revenue/pct, parts_margin/pct,
  bonus, fines, advance, total auto-calculat
- payroll_adjustments: bonus/fine/advance per period

PayrollCalculator service:
- compute($userId, $period) — calculează auto:
  - Manopere finalizate de mecanic în luna respectivă (sum total)
  - Marja pieselor montate de el (sell-buy * qty)
  - Bonus + fines + advance from adjustments
  - Total = base + works% + parts% + bonus - fines - advance

Resources Filament (group Finanțe):
- EmployeeProfileResource: profil cu % comisioane
- PayrollRunResource: salarii cu action 'Calculează luna curentă' (toți userii)
  + per-row 'Recalculează'; Sum summary pe total
- PayrollAdjustmentResource: gestionare bonus/penalizări/avansuri

══════ Cleanup ══════
- Șterse toate /__debug, /__seed, /__try-login, /__force-login, /__whoami,
  /__coolify-check (security)
- Routes/web.php conține doar / redirect, /manifest.json, /sw.js

Total Filament tenant routes: 92.
2026-05-07 09:52:01 +00:00
Vasyka 8d82af2f54 Faza 3.5+3.6+4+5: Marketing, Reports, Provisioning, PWA
═══ Faza 3.5: Marketing ═══
Schema: msg_templates, marketing_channels, calls
Modele cu logică:
- MessageTemplate::render($context) — substituie {key} tokens
- MarketingChannel: roi/conversion_rate/cost_per_lead computed attrs
- Call: duration_formatted helper

Resources Filament (group Marketing):
- MessageTemplateResource: 5 canale (telegram/whatsapp/viber/sms/email)
- MarketingChannelResource: budget vs revenue cu ROI live calculat
- CallResource: in/out/missed cu filtre azi/missed

═══ Faza 3.6: Analytics ═══
Custom Filament Page Reports cu 6 rapoarte tab-uite:
- Finanțe: încasări/cheltuieli/profit/datorii + breakdown pe metodă/categorie
- Încărcare: fișe deschise/închise + breakdown pe status
- Mecanici: ore lucrate, manopere, venit per mecanic
- Manopere top: cele mai frecvente cu nr/ore/venit
- Piese: top vândute + low-stock
- Clienți: noi în perioadă + lead-uri pe sursă
Selector perioadă: azi / săptămâna / luna / luna trecută / anul

═══ Faza 4: Central provisioning ═══
- CoolifyClient service (Coolify v4 REST API wrapper)
- CompanyProvisioner: creează Company + admin user + roles + adaugă
  subdomeniul în Coolify FQDN + trigger redeploy automat
- CreateCompany page override → folosește provisioner, returnează
  notificare cu credentialele admin
- Form CompanyResource extins cu admin_name/email/password (vizibil doar create)
- Action 'Suspendă' / 'Activează' pe table cu confirmation

Env vars necesare în Coolify pentru provisioning auto:
  COOLIFY_API_URL=http://65.21.20.141:8000
  COOLIFY_API_TOKEN=<token>
  COOLIFY_APP_UUID=g13hlrpd5g44zxl5af3ktio2

═══ Faza 5: PWA + branding ═══
- Route /manifest.json dinamic per tenant (nume, theme color, icons)
- Route /sw.js — service worker minimal (cache shell + static)
- TenantPanelProvider renderHook HEAD_END — link manifest + theme-color
  + apple-mobile-web-app meta
- TenantPanelProvider renderHook BODY_END — registrare service worker

Seed extins:
- 5 template-uri mesaje (programare/auto-gata/reminder/ITP/felicitare)
- 5 canale marketing (Google Ads/FB/IG/Telegram/Recomandări)
- 2 apeluri demo

Total Filament tenant routes: 81.
2026-05-07 04:55:33 +00:00
Vasyka f0f9fdd555 Faza 3.4: Finanțe — Plăți + Cheltuieli + Cashflow
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.
2026-05-06 22:55:50 +00:00
Vasyka 721c57ff97 Filament v5: Forms\Components\Section → Schemas\Components\Section, Forms\Get → Schemas\Components\Utilities\Get
Layout components (Section, Grid, Tabs, etc.) au fost mutate din
Filament\Forms\Components în Filament\Schemas\Components.
Forms\Components păstrează doar field-urile (TextInput, Select, etc.).

Forms\Get s-a mutat în Schemas\Components\Utilities\Get.
2026-05-06 18:07:10 +00:00
Vasyka c9cb3560ef Faza 3.1: CRM core — Leads, Deals, Appointments, Settings, Widgets, Users
Spatie Permission cu teams (team_foreign_key=company_id, teams=true):
- migrations create_permission_tables (model_has_roles cu company_id scope)
- HasRoles trait pe User
- ResolveTenant middleware setează permissions team_id la tenant.id
- Seed: 7 roluri default per tenant (admin/manager/receptionist/mechanic/parts_manager/accountant/marketer)

Module noi:
- Leads (cereri): name, phone, car/model, source, UTM, status, budget, assigned_to,
  acțiune "Convertește" → creează automat Client + Deal
- Deals (pipeline): client/vehicle, stage (8 stage-uri), price, source, lost_reason
- Posts + Appointments: post_id (boxă), master_id, date+time_start+time_end, status, color
- UserResource (tenant): CRUD users cu role/status/locale; canViewAny doar pentru admin

Custom Filament page "Setări" (tenant):
- Brand & contact (display_name, city, phone, email)
- Localizare (limba RO/RU/EN, currency, theme color picker)
- Servicii & tarif (labor_rate)
- Liste configurabile (services, cars) — păstrate în companies.settings JSON

Widgets dashboard:
- Tenant: StatsOverview (Clienți, Mașini, Cereri noi, Deal-uri active, Programări azi)
- Central: PlatformStats (Companii total/active/trial, Expiră în 7 zile)

Seed extins demo PSauto:
- 3 posturi (Pod 1/2/3 cu culori)
- 2 lead-uri demo (Alex Grosu Telegram, Irina Cojocaru WhatsApp)
- 3 deal-uri demo (BMW done, Audi in_work, Porsche agree)
- 2 programări (azi + mâine)

Filament v5 fixes:
- $navigationGroup type → string|UnitEnum|null (parent stricter signature)
- Toate resources noi au tipurile corecte
2026-05-06 17:36:32 +00:00