3 Commits

Author SHA1 Message Date
Vasyka 113610ea8f feat: WO apply_margin at fișă level + full RO/RU i18n on client portal
Two changes in one commit:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-13 04:56:04 +00:00
Vasyka 8fdfc9ef85 feat: Part product images + seasonal tire-swap reminders
Part (HasMedia):
- Spatie media `image` single-file collection + imageUrl() helper
- PartResource form: image upload section (image editor, 2 MB max)
- Parts list: circular thumbnail column
- Shop catalog cards: square thumbnail + 📦 placeholder
- Shop part detail: 260px image alongside info, single column when no image

Seasonal tire-swap reminders:
- NotificationDispatcher::tireSeasonalSwap(TireSet) — Telegram first, email
  fallback (when set has a vehicle, via ServiceReminderMail with 'tire_swap'
  type and a size-aware note)
- tires:remind-seasonal artisan command, self-gating to Feb 15-Mar 15
  (notify winter sets stored) and Sep 15-Oct 15 (notify summer sets stored).
  60-day cooldown per client via service_reminders_sent. --force / --dry-run.
- Schedule: weekly Mon 09:30

Tests (6 new):
- outside window no-ops; spring window notifies winter; spring ignores summer;
  autumn notifies summer; cooldown blocks doubles; --force overrides window

Full suite: 106 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 19:31:24 +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