Files
Vasyka 47eb4f62c1 feat(injector-protocols): full module — resource + PDF export via Browsershot
Implements the injector diagnostic protocol module per spec:
- Migration: injector_protocols + injector_protocol_rows with company_id
  (multi-tenant), vehicle_id (not car_id — this CRM uses Vehicle),
  master_id FK to users, snapshot fields (car_model/plate/vin/mileage/year/
  injector_brand) captured at protocol time.
- Models with BelongsToTenant; auto-generate protocol_number as
  PS-{tenantId}-{year}-{seq6}; auto-seed 8 empty rows on create.
- Permissions: injector_protocols.view + injector_protocols.conclude.
  Default roles: owner/admin (both), manager (both), receptionist (view
  only), mechanic (both). Test permission count updated 52→54.
- InjectorProtocolResource under Service nav group with 4 sections:
  data auto+client (with client_id/vehicle_id lookups auto-filling
  snapshot fields), reason checkboxes, 8-row repeater for measurements
  (Repeater with position hidden; fixed 8, non-addable/deletable),
  conclusion + comment + master signature.
- Table with columns, badge filters, PDF action.
- Blade PDF template: pixel-close to reference (graphite/gold/blue
  brand tokens, striped rows, rotated 'ФОРСУНКИ' header).
- Route /app/injector-protocols/{id}/pdf with permission gate,
  streams PDF via new InjectorProtocolPdfService (Browsershot).
- Dockerfile: install nodejs 22 + chromium + noto fonts + puppeteer-core
  in /opt/browsershot; env vars for Browsershot binary paths.
- +71 RU/EN translations covering resource + PDF template.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-26 11:27:53 +00:00

296 lines
14 KiB
PHP

<?php
namespace App\Auth;
/**
* Central permission catalog — 50 fine-grained permissions across 9 modules.
* Mirrors the catalog in /tmp/service/new/01-TZ-rbac-utilizator-tehnician.md §2.3.
*
* Use these constants instead of magic strings:
* $user->can(Permissions::WORK_ORDERS_CREATE)
*/
class Permissions
{
// Clients
public const CLIENTS_VIEW_ALL = 'clients.view_all';
public const CLIENTS_VIEW_OWN = 'clients.view_own';
public const CLIENTS_CREATE = 'clients.create';
public const CLIENTS_EDIT = 'clients.edit';
public const CLIENTS_DELETE = 'clients.delete';
public const CLIENTS_EXPORT = 'clients.export';
// Vehicles
public const VEHICLES_VIEW_ALL = 'vehicles.view_all';
public const VEHICLES_CREATE = 'vehicles.create';
public const VEHICLES_EDIT = 'vehicles.edit';
public const VEHICLES_DELETE = 'vehicles.delete';
// Work orders (Fișe)
public const WORK_ORDERS_VIEW_ALL = 'work_orders.view_all';
public const WORK_ORDERS_VIEW_OWN_ASSIGNED = 'work_orders.view_own_assigned';
public const WORK_ORDERS_CREATE = 'work_orders.create';
public const WORK_ORDERS_EDIT = 'work_orders.edit';
public const WORK_ORDERS_DELETE = 'work_orders.delete';
public const WORK_ORDERS_CHANGE_STATUS = 'work_orders.change_status';
public const WORK_ORDERS_APPROVE_DISCOUNT_5 = 'work_orders.approve_discount_5';
public const WORK_ORDERS_APPROVE_DISCOUNT_20 = 'work_orders.approve_discount_20';
public const WORK_ORDERS_APPROVE_DISCOUNT_ANY = 'work_orders.approve_discount_any';
public const WORK_ORDERS_PRINT = 'work_orders.print';
// Finance
public const FINANCE_VIEW_OVERVIEW = 'finance.view_overview';
public const FINANCE_VIEW_PL = 'finance.view_pl';
public const FINANCE_VIEW_INTERNAL_MARGIN = 'finance.view_internal_margin';
public const FINANCE_CREATE_PAYMENT = 'finance.create_payment';
public const FINANCE_DELETE_PAYMENT = 'finance.delete_payment';
public const FINANCE_CREATE_EXPENSE = 'finance.create_expense';
public const FINANCE_EXPORT = 'finance.export';
// Salaries
public const SALARIES_VIEW_OWN = 'salaries.view_own';
public const SALARIES_VIEW_ALL = 'salaries.view_all';
public const SALARIES_CALCULATE = 'salaries.calculate';
public const SALARIES_MARK_PAID = 'salaries.mark_paid';
// Inventory
public const INVENTORY_VIEW = 'inventory.view';
public const INVENTORY_CREATE_PART = 'inventory.create_part';
public const INVENTORY_EDIT_PART = 'inventory.edit_part';
public const INVENTORY_DELETE_PART = 'inventory.delete_part';
public const INVENTORY_ADJUST_STOCK = 'inventory.adjust_stock';
public const INVENTORY_CREATE_PURCHASE = 'inventory.create_purchase';
public const INVENTORY_RECEIVE_GOODS = 'inventory.receive_goods';
// Suppliers
public const SUPPLIERS_VIEW = 'suppliers.view';
public const SUPPLIERS_EDIT = 'suppliers.edit';
public const SUPPLIERS_DELETE = 'suppliers.delete';
// Admin
public const ADMIN_USERS_VIEW = 'admin.users.view';
public const ADMIN_USERS_MANAGE = 'admin.users.manage';
public const ADMIN_ROLES_MANAGE = 'admin.roles.manage';
public const ADMIN_SETTINGS_EDIT = 'admin.settings.edit';
public const ADMIN_INTEGRATIONS = 'admin.settings.integrations';
public const ADMIN_API_TOKENS_MANAGE = 'admin.api_tokens.manage';
public const ADMIN_AUDIT_LOG_VIEW = 'admin.audit_log.view';
public const ADMIN_BACKUP_DOWNLOAD = 'admin.backup.download';
// AI & Analytics
public const AI_ASSISTANT_USE = 'ai_assistant.use';
public const AI_ASSISTANT_CONFIGURE_KEYS = 'ai_assistant.configure_keys';
public const ANALYTICS_VIEW = 'analytics.view';
// Injector diagnostic protocols
public const INJECTOR_PROTOCOLS_VIEW = 'injector_protocols.view';
public const INJECTOR_PROTOCOLS_CONCLUDE = 'injector_protocols.conclude';
/** Full list — used by seeder. */
public static function all(): array
{
return [
self::CLIENTS_VIEW_ALL, self::CLIENTS_VIEW_OWN, self::CLIENTS_CREATE, self::CLIENTS_EDIT,
self::CLIENTS_DELETE, self::CLIENTS_EXPORT,
self::VEHICLES_VIEW_ALL, self::VEHICLES_CREATE, self::VEHICLES_EDIT, self::VEHICLES_DELETE,
self::WORK_ORDERS_VIEW_ALL, self::WORK_ORDERS_VIEW_OWN_ASSIGNED, self::WORK_ORDERS_CREATE,
self::WORK_ORDERS_EDIT, self::WORK_ORDERS_DELETE, self::WORK_ORDERS_CHANGE_STATUS,
self::WORK_ORDERS_APPROVE_DISCOUNT_5, self::WORK_ORDERS_APPROVE_DISCOUNT_20,
self::WORK_ORDERS_APPROVE_DISCOUNT_ANY, self::WORK_ORDERS_PRINT,
self::FINANCE_VIEW_OVERVIEW, self::FINANCE_VIEW_PL, self::FINANCE_VIEW_INTERNAL_MARGIN, self::FINANCE_CREATE_PAYMENT,
self::FINANCE_DELETE_PAYMENT, self::FINANCE_CREATE_EXPENSE, self::FINANCE_EXPORT,
self::SALARIES_VIEW_OWN, self::SALARIES_VIEW_ALL, self::SALARIES_CALCULATE, self::SALARIES_MARK_PAID,
self::INVENTORY_VIEW, self::INVENTORY_CREATE_PART, self::INVENTORY_EDIT_PART, self::INVENTORY_DELETE_PART,
self::INVENTORY_ADJUST_STOCK, self::INVENTORY_CREATE_PURCHASE, self::INVENTORY_RECEIVE_GOODS,
self::SUPPLIERS_VIEW, self::SUPPLIERS_EDIT, self::SUPPLIERS_DELETE,
self::ADMIN_USERS_VIEW, self::ADMIN_USERS_MANAGE, self::ADMIN_ROLES_MANAGE,
self::ADMIN_SETTINGS_EDIT, self::ADMIN_INTEGRATIONS, self::ADMIN_API_TOKENS_MANAGE,
self::ADMIN_AUDIT_LOG_VIEW, self::ADMIN_BACKUP_DOWNLOAD,
self::AI_ASSISTANT_USE, self::AI_ASSISTANT_CONFIGURE_KEYS, self::ANALYTICS_VIEW,
self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE,
];
}
/** Groups for UI rendering: module → list of permissions. */
public static function grouped(): array
{
$groups = [];
foreach (self::all() as $slug) {
[$module] = explode('.', $slug, 2);
$groups[$module][] = $slug;
}
return $groups;
}
public static function labels(): array
{
return [
'clients' => 'Clienți',
'vehicles' => 'Mașini',
'work_orders' => 'Fișe lucru',
'finance' => 'Finanțe',
'salaries' => 'Salarii',
'inventory' => 'Stoc',
'suppliers' => 'Furnizori',
'admin' => 'Administrare',
'ai_assistant' => 'AI Assistant',
'analytics' => 'Analitică',
'injector_protocols' => 'Protocol forsunki',
];
}
/** Human-readable label for each permission slug. Used by role editor + audit UI. */
public static function permissionLabels(): array
{
return [
'clients.view_all' => 'Vezi toți clienții',
'clients.view_own' => 'Vezi doar clienții proprii',
'clients.create' => 'Adaugă client',
'clients.edit' => 'Editează client',
'clients.delete' => 'Șterge client',
'clients.export' => 'Exportă clienți (CSV)',
'vehicles.view_all' => 'Vezi toate mașinile',
'vehicles.create' => 'Adaugă mașină',
'vehicles.edit' => 'Editează mașină',
'vehicles.delete' => 'Șterge mașină',
'work_orders.view_all' => 'Vezi toate fișele',
'work_orders.view_own_assigned' => 'Vezi doar fișele proprii asignate',
'work_orders.create' => 'Deschide fișă nouă',
'work_orders.edit' => 'Editează fișă',
'work_orders.delete' => 'Șterge fișă',
'work_orders.change_status' => 'Schimbă statusul fișei',
'work_orders.approve_discount_5' => 'Aprobă discount ≤ 5%',
'work_orders.approve_discount_20' => 'Aprobă discount ≤ 20%',
'work_orders.approve_discount_any' => 'Aprobă discount fără limită',
'work_orders.print' => 'Tipărește / PDF fișă',
'finance.view_overview' => 'Vezi panou finanțe',
'finance.view_pl' => 'Vezi profit & pierdere (P&L)',
'finance.view_internal_margin' => 'Vezi marja internă (baza de salariu)',
'finance.create_payment' => 'Înregistrează plată',
'finance.delete_payment' => 'Șterge plată',
'finance.create_expense' => 'Înregistrează cheltuială',
'finance.export' => 'Exportă rapoarte financiare',
'salaries.view_own' => 'Vezi propriul salariu',
'salaries.view_all' => 'Vezi salariile tuturor',
'salaries.calculate' => 'Calculează salarii',
'salaries.mark_paid' => 'Marchează salariul ca plătit',
'inventory.view' => 'Vezi stoc (piese)',
'inventory.create_part' => 'Adaugă piesă în catalog',
'inventory.edit_part' => 'Editează piesă',
'inventory.delete_part' => 'Șterge piesă',
'inventory.adjust_stock' => 'Ajustează stocul manual',
'inventory.create_purchase' => 'Creează comandă de aprovizionare',
'inventory.receive_goods' => 'Recepționează marfă',
'suppliers.view' => 'Vezi furnizori',
'suppliers.edit' => 'Editează furnizor',
'suppliers.delete' => 'Șterge furnizor',
'admin.users.view' => 'Vezi utilizatori',
'admin.users.manage' => 'Gestionează utilizatori (creează/blochează)',
'admin.roles.manage' => 'Editează roluri & drepturi',
'admin.settings.edit' => 'Editează setările companiei',
'admin.settings.integrations' => 'Configurează integrări externe',
'admin.api_tokens.manage' => 'Gestionează tokene API',
'admin.audit_log.view' => 'Vezi jurnalul de audit',
'admin.backup.download' => 'Descarcă backup',
'ai_assistant.use' => 'Folosește AI Assistant',
'ai_assistant.configure_keys' => 'Configurează chei API pentru AI',
'analytics.view' => 'Vezi rapoarte & analitică',
'injector_protocols.view' => 'Vezi protocoalele de diagnostic forsunki',
'injector_protocols.conclude' => 'Completează Заключение (concluzii + semnătură)',
];
}
/**
* Role-permission matrix per TZ §2.4.
* Returns: ['owner' => [perm1, perm2, ...], ...]
*/
public static function roleMatrix(): array
{
$all = self::all();
$matrix = [];
// owner = everything
$matrix['owner'] = $all;
// admin = everything except billing-only stuff (we don't have that yet, so = all)
$matrix['admin'] = $all;
// manager: operations + reporting, no destructive
$matrix['manager'] = [
self::CLIENTS_VIEW_ALL, self::CLIENTS_CREATE, self::CLIENTS_EDIT, self::CLIENTS_EXPORT,
self::VEHICLES_VIEW_ALL, self::VEHICLES_CREATE, self::VEHICLES_EDIT,
self::WORK_ORDERS_VIEW_ALL, self::WORK_ORDERS_VIEW_OWN_ASSIGNED, self::WORK_ORDERS_CREATE,
self::WORK_ORDERS_EDIT, self::WORK_ORDERS_CHANGE_STATUS,
self::WORK_ORDERS_APPROVE_DISCOUNT_5, self::WORK_ORDERS_APPROVE_DISCOUNT_20,
self::WORK_ORDERS_PRINT,
self::FINANCE_VIEW_OVERVIEW, self::FINANCE_VIEW_INTERNAL_MARGIN, self::FINANCE_CREATE_PAYMENT, self::FINANCE_CREATE_EXPENSE,
self::SALARIES_VIEW_OWN,
self::INVENTORY_VIEW, self::INVENTORY_CREATE_PART, self::INVENTORY_EDIT_PART,
self::INVENTORY_ADJUST_STOCK, self::INVENTORY_CREATE_PURCHASE, self::INVENTORY_RECEIVE_GOODS,
self::SUPPLIERS_VIEW, self::SUPPLIERS_EDIT,
self::AI_ASSISTANT_USE,
self::ANALYTICS_VIEW,
self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE,
];
// accountant: finance + reporting only
$matrix['accountant'] = [
self::CLIENTS_VIEW_ALL, self::CLIENTS_EXPORT,
self::VEHICLES_VIEW_ALL,
self::WORK_ORDERS_VIEW_ALL,
self::FINANCE_VIEW_OVERVIEW, self::FINANCE_VIEW_PL, self::FINANCE_CREATE_PAYMENT,
self::FINANCE_DELETE_PAYMENT, self::FINANCE_CREATE_EXPENSE, self::FINANCE_EXPORT,
self::SALARIES_VIEW_OWN, self::SALARIES_VIEW_ALL, self::SALARIES_CALCULATE, self::SALARIES_MARK_PAID,
self::INVENTORY_VIEW,
self::SUPPLIERS_VIEW,
self::ANALYTICS_VIEW,
self::AI_ASSISTANT_USE,
];
// receptionist: front-desk operations
$matrix['receptionist'] = [
self::CLIENTS_VIEW_ALL, self::CLIENTS_CREATE, self::CLIENTS_EDIT,
self::VEHICLES_VIEW_ALL, self::VEHICLES_CREATE, self::VEHICLES_EDIT,
self::WORK_ORDERS_VIEW_ALL, self::WORK_ORDERS_CREATE, self::WORK_ORDERS_EDIT,
self::WORK_ORDERS_CHANGE_STATUS, self::WORK_ORDERS_APPROVE_DISCOUNT_5, self::WORK_ORDERS_PRINT,
self::FINANCE_CREATE_PAYMENT,
self::SALARIES_VIEW_OWN,
self::INVENTORY_VIEW,
self::AI_ASSISTANT_USE,
self::INJECTOR_PROTOCOLS_VIEW,
];
// mechanic: only own WOs + inventory view
$matrix['mechanic'] = [
self::WORK_ORDERS_VIEW_OWN_ASSIGNED, self::WORK_ORDERS_CHANGE_STATUS,
self::INVENTORY_VIEW,
self::SALARIES_VIEW_OWN,
self::INJECTOR_PROTOCOLS_VIEW, self::INJECTOR_PROTOCOLS_CONCLUDE,
];
// viewer: read-only
$matrix['viewer'] = [
self::CLIENTS_VIEW_ALL,
self::VEHICLES_VIEW_ALL,
self::WORK_ORDERS_VIEW_ALL,
self::INVENTORY_VIEW,
self::SUPPLIERS_VIEW,
self::AI_ASSISTANT_USE,
];
return $matrix;
}
public static function roleLabels(): array
{
return [
'owner' => 'Proprietar',
'admin' => 'Administrator',
'manager' => 'Manager',
'accountant' => 'Contabil',
'receptionist' => 'Recepție',
'mechanic' => 'Mecanic',
'viewer' => 'Vizitator',
];
}
}