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>
This commit is contained in:
2026-07-13 04:56:04 +00:00
parent f5ff3f149a
commit 113610ea8f
25 changed files with 495 additions and 166 deletions
+155
View File
@@ -0,0 +1,155 @@
<?php
return [
// ─── Common ───
'common' => [
'email' => 'Email',
'phone' => 'Telefon',
'name' => 'Nume',
'password' => 'Parolă',
'save' => 'Salvează',
'cancel' => 'Anulează',
'back' => 'Înapoi',
'search' => 'Caută',
'total' => 'Total',
'yes' => 'Da',
'no' => 'Nu',
'required' => '*',
'currency_mdl' => 'MDL',
'powered_by' => 'Powered by AutoCRM',
],
// ─── Invitation flow ───
'invitation' => [
'title_accept' => 'Acceptă invitația',
'welcome_name' => 'Bine ai venit, :name!',
'welcome_body' => 'Ai fost invitat să accesezi :company. Setează o parolă pentru a-ți activa contul.',
'new_password' => 'Parolă nouă',
'confirm_password' => 'Confirmă parola',
'activate_account' => 'Activează contul',
'expired_title' => 'Invitație expirată',
'expired_body' => 'Linkul de invitație a expirat (durata maximă: 7 zile). Roagă administratorul să retrimită invitația.',
'invalid_title' => 'Invitație invalidă',
'invalid_body' => 'Linkul nu mai este valabil sau a fost deja folosit.',
],
// ─── WO tracking (public) ───
'tracking' => [
'title_fisa' => 'Fișa #:number',
'ready_estimated' => 'Gata estimat:',
'details' => 'Detalii',
'auto' => 'Auto',
'mileage' => 'Kilometraj',
'opened' => 'Deschis',
'master' => 'Maistru',
'stages' => 'Etape',
'what_asked' => 'Ce ne-ai cerut',
'recommendations' => 'Recomandări',
'photos' => 'Fotografii',
'approval_needed_title' => '⚠ Necesită aprobarea ta',
'approval_needed_body' => 'Am descoperit lucrări suplimentare. Te rugăm să decizi mai jos.',
'hours' => 'ore',
'approve' => '✅ Aprob',
'decline' => '❌ Nu aprob',
'unit_pcs' => 'buc',
],
// ─── Shop navigation & layout ───
'shop' => [
'catalog' => 'Catalog',
'search_by_vin' => 'Caută după VIN',
'cart' => '🛒 Coș',
'login' => 'Login',
'register' => 'Înregistrare',
'logout' => 'Ieșire',
// Catalog
'catalog_title' => 'Catalog piese',
'search_placeholder' => 'Caută denumire, cod, brand, cod cross…',
'all_categories' => 'Toate categoriile',
'in_stock_only' => 'Doar în stoc',
'no_parts_found' => 'Nicio piesă găsită',
'no_parts_found_for' => 'Nicio piesă găsită pentru „:term"',
'in_stock' => '● În stoc',
'on_order' => '○ La comandă',
'add_to_cart' => 'Adaugă în coș',
// Cart
'cart_title' => 'Coșul meu',
'cart_empty' => 'Coșul e gol.',
'view_catalog' => 'Vezi catalogul',
'part' => 'Piesă',
'price' => 'Preț',
'qty' => 'Cant.',
'update_cart' => 'Actualizează coșul',
'subtotal' => 'Subtotal:',
'checkout_arrow' => 'Finalizează comanda →',
// Checkout
'checkout_title' => 'Finalizează comanda',
'full_name' => 'Nume complet *',
'phone_req' => 'Telefon *',
'delivery_req' => 'Livrare *',
'address_delivery' => 'Adresă (pentru curier/poștă)',
'notes' => 'Observații',
'place_order' => 'Plasează comanda',
'summary' => 'Sumar',
'delivery_fee_note' => 'Taxa de livrare se calculează în funcție de metoda aleasă.',
// Order status
'order_number' => 'Comanda',
'status' => 'Status',
'products' => 'Produse',
'delivery' => 'Livrare',
'delivery_data' => 'Date livrare',
'continue_shopping' => '← Continuă cumpărăturile',
// Part detail
'back_to_catalog' => '← Înapoi la catalog',
'brand_label' => 'Brand:',
'code_label' => 'Cod:',
'in_stock_qty' => '● În stoc (:qty :unit)',
'cross_refs_title' => 'Coduri echivalente (cross)',
'description' => 'Descriere',
// VIN
'vin_title' => 'Caută piese după VIN',
'vin_intro' => 'Introdu codul VIN (17 caractere) ca să identificăm mașina. Apoi caută piesele în catalog.',
'vin_decode' => 'Decodează',
'vin_invalid' => 'VIN invalid — trebuie 17 caractere.',
'vin_identified' => 'Mașină identificată',
'vin_manufacturer' => 'Producător',
'vin_year' => 'An model',
'vin_country' => 'Țară',
'vin_region' => 'Regiune',
'vin_search_parts' => 'Caută piese pentru :maker →',
'vin_contact_note' => 'Pentru compatibilitate exactă pe model/motorizare, contactează service-ul cu acest VIN.',
'this_car' => 'această mașină',
// Account
'my_account' => 'Contul meu',
'hello_name' => 'Salut, :name!',
'contact_data' => 'Date contact',
'my_orders' => 'Comenzile mele (:count)',
'no_orders_yet' => 'Nu ai nicio comandă încă.',
'order_nr' => 'Nr.',
'order_date' => 'Data',
'order_items' => 'Articole',
'order_details_arrow' => 'Detalii →',
// Auth
'signin_title' => 'Intră în cont',
'signin_button' => 'Intră',
'forgot_password' => 'Am uitat parola',
'no_account' => 'Nu ai cont?',
'register_title' => 'Înregistrare cont',
'register_button' => 'Creează cont',
'have_account' => 'Ai deja cont?',
'forgot_title' => 'Am uitat parola',
'forgot_intro' => 'Introdu emailul cu care te-ai înregistrat — îți trimitem un link de resetare.',
'send_reset_link' => 'Trimite link resetare',
'back_to_login' => '← Înapoi la login',
'reset_title' => 'Setează o parolă nouă',
'reset_button' => 'Setează parola',
],
];
+155
View File
@@ -0,0 +1,155 @@
<?php
return [
// ─── Общие ───
'common' => [
'email' => 'Email',
'phone' => 'Телефон',
'name' => 'Имя',
'password' => 'Пароль',
'save' => 'Сохранить',
'cancel' => 'Отмена',
'back' => 'Назад',
'search' => 'Поиск',
'total' => 'Итого',
'yes' => 'Да',
'no' => 'Нет',
'required' => '*',
'currency_mdl' => 'MDL',
'powered_by' => 'Работает на AutoCRM',
],
// ─── Приглашение ───
'invitation' => [
'title_accept' => 'Принять приглашение',
'welcome_name' => 'Добро пожаловать, :name!',
'welcome_body' => 'Вас пригласили в :company. Задайте пароль, чтобы активировать аккаунт.',
'new_password' => 'Новый пароль',
'confirm_password' => 'Подтвердите пароль',
'activate_account' => 'Активировать аккаунт',
'expired_title' => 'Приглашение просрочено',
'expired_body' => 'Срок действия ссылки истёк (максимум 7 дней). Попросите администратора выслать приглашение повторно.',
'invalid_title' => 'Недействительное приглашение',
'invalid_body' => 'Ссылка больше не действительна или уже была использована.',
],
// ─── Отслеживание заказ-наряда (публично) ───
'tracking' => [
'title_fisa' => 'Заказ-наряд #:number',
'ready_estimated' => 'Готово ориентировочно:',
'details' => 'Подробности',
'auto' => 'Автомобиль',
'mileage' => 'Пробег',
'opened' => 'Открыт',
'master' => 'Мастер',
'stages' => 'Этапы',
'what_asked' => 'Что вы просили',
'recommendations' => 'Рекомендации',
'photos' => 'Фотографии',
'approval_needed_title' => '⚠ Требуется ваше подтверждение',
'approval_needed_body' => 'Обнаружены дополнительные работы. Пожалуйста, примите решение ниже.',
'hours' => 'ч.',
'approve' => '✅ Согласен',
'decline' => '❌ Не согласен',
'unit_pcs' => 'шт.',
],
// ─── Магазин: навигация и лейаут ───
'shop' => [
'catalog' => 'Каталог',
'search_by_vin' => 'Поиск по VIN',
'cart' => '🛒 Корзина',
'login' => 'Войти',
'register' => 'Регистрация',
'logout' => 'Выйти',
// Каталог
'catalog_title' => 'Каталог запчастей',
'search_placeholder' => 'Найти по названию, коду, бренду, кросс-номеру…',
'all_categories' => 'Все категории',
'in_stock_only' => 'Только в наличии',
'no_parts_found' => 'Запчасти не найдены',
'no_parts_found_for' => 'Запчасти не найдены по запросу „:term"',
'in_stock' => '● В наличии',
'on_order' => '○ Под заказ',
'add_to_cart' => 'Добавить в корзину',
// Корзина
'cart_title' => 'Моя корзина',
'cart_empty' => 'Корзина пуста.',
'view_catalog' => 'Перейти в каталог',
'part' => 'Запчасть',
'price' => 'Цена',
'qty' => 'Кол-во',
'update_cart' => 'Обновить корзину',
'subtotal' => 'Промежуточный итог:',
'checkout_arrow' => 'Оформить заказ →',
// Оформление
'checkout_title' => 'Оформление заказа',
'full_name' => 'ФИО *',
'phone_req' => 'Телефон *',
'delivery_req' => 'Доставка *',
'address_delivery' => 'Адрес (для курьера/почты)',
'notes' => 'Комментарий',
'place_order' => 'Оформить заказ',
'summary' => 'Сводка',
'delivery_fee_note' => 'Стоимость доставки рассчитывается в зависимости от выбранного способа.',
// Статус заказа
'order_number' => 'Заказ',
'status' => 'Статус',
'products' => 'Товары',
'delivery' => 'Доставка',
'delivery_data' => 'Данные доставки',
'continue_shopping' => '← Продолжить покупки',
// Детали запчасти
'back_to_catalog' => '← Назад в каталог',
'brand_label' => 'Бренд:',
'code_label' => 'Код:',
'in_stock_qty' => '● В наличии (:qty :unit)',
'cross_refs_title' => 'Кросс-номера',
'description' => 'Описание',
// VIN
'vin_title' => 'Поиск запчастей по VIN',
'vin_intro' => 'Введите VIN-код (17 символов), чтобы мы определили автомобиль. Затем найдите запчасти в каталоге.',
'vin_decode' => 'Расшифровать',
'vin_invalid' => 'Неверный VIN — должно быть 17 символов.',
'vin_identified' => 'Автомобиль определён',
'vin_manufacturer' => 'Производитель',
'vin_year' => 'Год модели',
'vin_country' => 'Страна',
'vin_region' => 'Регион',
'vin_search_parts' => 'Найти запчасти для :maker →',
'vin_contact_note' => 'Для точного подбора по модели/двигателю свяжитесь с сервисом, указав этот VIN.',
'this_car' => 'этого автомобиля',
// Аккаунт
'my_account' => 'Мой аккаунт',
'hello_name' => 'Привет, :name!',
'contact_data' => 'Контактные данные',
'my_orders' => 'Мои заказы (:count)',
'no_orders_yet' => 'У вас пока нет заказов.',
'order_nr' => '№',
'order_date' => 'Дата',
'order_items' => 'Позиции',
'order_details_arrow' => 'Подробнее →',
// Аутентификация
'signin_title' => 'Войти в аккаунт',
'signin_button' => 'Войти',
'forgot_password' => 'Забыли пароль',
'no_account' => 'Нет аккаунта?',
'register_title' => 'Регистрация аккаунта',
'register_button' => 'Создать аккаунт',
'have_account' => 'Уже есть аккаунт?',
'forgot_title' => 'Забыли пароль',
'forgot_intro' => 'Введите email, с которым регистрировались — вышлем ссылку для сброса.',
'send_reset_link' => 'Выслать ссылку',
'back_to_login' => '← Вернуться ко входу',
'reset_title' => 'Задать новый пароль',
'reset_button' => 'Установить пароль',
],
];