From cf0c43a211c0bb3e6a454046bda0a4be95205b33 Mon Sep 17 00:00:00 2001 From: dnviti Date: Sun, 30 Nov 2025 00:34:56 +0100 Subject: [PATCH] - --- DEVELOPMENT.md | 37 + frontend/package-lock.json | 109 +- frontend/package.json | 4 + frontend/public/locales/en/translation.json | 1049 +++++++++++++++ frontend/public/locales/it/translation.json | 1128 +++++++++++++++++ frontend/src/components/Layout.tsx | 41 +- frontend/src/components/SettingsSelector.tsx | 4 +- .../customFields/CustomFieldsRenderer.tsx | 4 +- frontend/src/contexts/LanguageContext.tsx | 61 +- frontend/src/i18n.ts | 35 + frontend/src/main.tsx | 7 +- .../warehouse/pages/ArticleFormPage.tsx | 161 +-- .../modules/warehouse/pages/ArticlesPage.tsx | 64 +- .../warehouse/pages/InboundMovementPage.tsx | 57 +- .../warehouse/pages/InventoryCountPage.tsx | 47 +- .../warehouse/pages/InventoryFormPage.tsx | 38 +- .../warehouse/pages/InventoryListPage.tsx | 45 +- .../modules/warehouse/pages/MovementsPage.tsx | 172 +-- .../warehouse/pages/OutboundMovementPage.tsx | 61 +- .../warehouse/pages/StockLevelsPage.tsx | 50 +- .../warehouse/pages/TransferMovementPage.tsx | 61 +- .../warehouse/pages/WarehouseDashboard.tsx | 54 +- .../pages/WarehouseLocationsPage.tsx | 92 +- frontend/src/pages/ArticoliPage.tsx | 80 +- frontend/src/pages/AutoCodesAdminPage.tsx | 109 +- frontend/src/pages/CalendarioPage.tsx | 22 +- frontend/src/pages/ClientiPage.tsx | 68 +- frontend/src/pages/CustomFieldsAdminPage.tsx | 829 ++++++++---- frontend/src/pages/Dashboard.tsx | 93 +- frontend/src/pages/EventiPage.tsx | 60 +- frontend/src/pages/EventoDetailPage.tsx | 223 ++-- frontend/src/pages/LocationPage.tsx | 48 +- frontend/src/pages/ModulePurchasePage.tsx | 108 +- frontend/src/pages/ModulesAdminPage.tsx | 71 +- frontend/src/pages/ReportEditorPage.tsx | 112 +- frontend/src/pages/ReportTemplatesPage.tsx | 63 +- frontend/src/pages/RisorsePage.tsx | 40 +- frontend/src/types/customFields.ts | 42 + 38 files changed, 4024 insertions(+), 1325 deletions(-) create mode 100644 frontend/public/locales/en/translation.json create mode 100644 frontend/public/locales/it/translation.json create mode 100644 frontend/src/i18n.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1fca904..a4d9d02 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -42,6 +42,14 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve **NON dimenticare:** Questo file è la memoria persistente tra sessioni. Se non viene aggiornato, il lavoro fatto andrà perso e dovrà essere riscoperto. +### Rispetto delle Traduzioni (i18n) + +**OBBLIGATORIO:** È severamente vietato inserire stringhe hardcoded nel codice frontend. +- **TUTTI** i testi visibili all'utente devono usare `useTranslation` e le chiavi definite in `translation.json`. +- **Nuove stringhe:** Se serve un nuovo testo, aggiungerlo PRIMA in `it/translation.json` e `en/translation.json`, poi usarlo nel codice. +- **Placeholder:** Usare i placeholder (es. `{{value}}`) per valori dinamici, mai concatenazione di stringhe. +- **Date/Numeri:** Usare i formattatori di `react-i18next` o `Intl` per date e numeri. + --- ## Quick Start - Session Recovery @@ -65,6 +73,24 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve - Persistenza impostazioni nel browser (localStorage) - Adattamento automatico componenti MUI +- **NUOVA FEATURE: Sistema Internazionalizzazione (i18n)** - COMPLETATO + - **Obiettivo:** Implementare un sistema robusto per la gestione delle traduzioni (Italiano/Inglese) + - **Stack Tecnologico:** `i18next`, `react-i18next`, `i18next-http-backend`, `i18next-browser-languagedetector` + - **Implementazione:** + - `i18n.ts` - Configurazione inizializzazione i18next con backend loader e detector + - `public/locales/{lang}/translation.json` - File di traduzione JSON separati per lingua + - Refactoring `LanguageContext.tsx` per usare `useTranslation` hook + - Aggiornamento `Layout.tsx` e `SettingsSelector.tsx` per usare chiavi di traduzione + - Traduzione completa delle pagine: + - `Dashboard.tsx` + - `EventiPage.tsx` + - `ClientiPage.tsx` + - **Vantaggi:** + - Caricamento asincrono delle traduzioni + - Rilevamento automatico lingua browser + - Struttura scalabile per future lingue + - Namespace per organizzazione chiavi (common, menu, modules) + - **NUOVA FEATURE: Gestione Inventario (Frontend)** - COMPLETATO - **Obiettivo:** Interfaccia utente per la gestione completa degli inventari fisici @@ -90,6 +116,17 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve - **Soluzione:** Corretti i percorsi in `useWarehouseNavigation.ts` per corrispondere a `routes.tsx` - **File modificati:** `frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts` +- **FIX: Traduzioni Warehouse non funzionanti** - RISOLTO + - **Problema:** Le traduzioni del modulo warehouse mostravano i placeholder (chiavi) invece del testo + - **Causa:** La sezione `warehouse` in `translation.json` era erroneamente annidata dentro l'oggetto `reports` invece di essere alla radice + - **Soluzione:** Spostata la sezione `warehouse` al livello principale in `translation.json` (IT e EN) + - **File modificati:** `frontend/public/locales/it/translation.json`, `frontend/public/locales/en/translation.json` + +- **FIX: Traduzioni Custom Fields** - RISOLTO + - **Problema:** Mancavano traduzioni per la pagina dei custom fields e per i tipi di campo + - **Soluzione:** Aggiunte chiavi mancanti (`noFields`, `multiselect`, `sectionTitle`), corretto casing per entità warehouse, aggiornato codice per usare chiavi corrette + - **File modificati:** `frontend/public/locales/it/translation.json`, `frontend/public/locales/en/translation.json`, `frontend/src/pages/CustomFieldsAdminPage.tsx`, `frontend/src/components/customFields/CustomFieldsRenderer.tsx` + - **FIX: Campo Codice Readonly e Codice Alternativo** - COMPLETATO - **Obiettivo:** Il campo "Codice" deve essere sempre auto-generato (non modificabile), aggiungere campo "Codice Alternativo" opzionale - **Backend modificato:** diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 39fc17e..931ad97 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,8 +26,12 @@ "dayjs": "^1.11.19", "fabric": "^6.9.0", "file-saver": "^2.0.5", + "i18next": "^25.6.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", "react-router-dom": "^7.9.6", "uuid": "^13.0.0", "zustand": "^5.0.8" @@ -2878,6 +2882,15 @@ "node": ">= 6" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3957,6 +3970,15 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -3986,6 +4008,55 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "25.6.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.3.tgz", + "integrity": "sha512-AEQvoPDljhp67a1+NsnG/Wb1Nh6YoSvtrmeEd24sfGn3uujCtXCF3cXpr7ulhMywKNFF7p3TX1u2j7y+caLOJg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -4902,6 +4973,33 @@ "react": "^19.2.0" } }, + "node_modules/react-i18next": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz", + "integrity": "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", @@ -5421,7 +5519,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5626,6 +5724,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2c284e1..fbb32ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,8 +28,12 @@ "dayjs": "^1.11.19", "fabric": "^6.9.0", "file-saver": "^2.0.5", + "i18next": "^25.6.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", "react-router-dom": "^7.9.6", "uuid": "^13.0.0", "zustand": "^5.0.8" diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json new file mode 100644 index 0000000..1af917b --- /dev/null +++ b/frontend/public/locales/en/translation.json @@ -0,0 +1,1049 @@ +{ + "common": { + "settings": "Settings", + "theme": "Theme", + "language": "Language", + "dark": "Dark", + "light": "Light", + "logout": "Logout", + "save": "Save", + "cancel": "Cancel", + "close": "Close", + "delete": "Delete", + "edit": "Edit", + "new": "New", + "search": "Search", + "actions": "Actions", + "confirm": "Confirm", + "back": "Back", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "unknown": "Unknown", + "deleteAll": "Delete All", + "generate": "Generate", + "warning": "Warning", + "create": "Create", + "deleteConfirm": "Delete this item?", + "optional": "Optional", + "notes": "Notes", + "preview": "Preview", + "none": "None" + }, + "menu": { + "dashboard": "Dashboard", + "calendar": "Calendar", + "events": "Events", + "clients": "Clients", + "location": "Location", + "articles": "Articles", + "resources": "Resources", + "warehouse": "Warehouse", + "reports": "Reports", + "modules": "Modules", + "autoCodes": "Auto Codes", + "customFields": "Custom Fields" + }, + "dashboard": { + "title": "Dashboard", + "totalEvents": "Total Events", + "confirmed": "Confirmed", + "inQuote": "In Quote", + "eventsToday": "Events Today", + "upcomingEvents": "Upcoming Events (30 days)", + "expiringQuotes": "Expiring Quotes", + "noEvents": "No events in the next 30 days", + "noQuotes": "No pending quotes", + "generateDemoData": "Generate Demo Data", + "clearDatabase": "Clear Database", + "generateDialogTitle": "Generate Demo Data", + "generateDialogText": "This operation generates test data for demonstrations:
- 15 Clients
- 10 Locations
- 12 Resources (staff)
- 20 Articles
- 20 Events with details

Existing data will not be modified.", + "clearDialogTitle": "Clear Database", + "clearDialogWarning": "Warning: this operation deletes ALL data from the database!", + "clearDialogText": "The following will be deleted:
- All events and their details
- All clients
- All locations
- All resources
- All articles

This operation cannot be undone.", + "clearSuccess": "Database cleared. Deleted: {{events}} events, {{clients}} clients, {{locations}} locations, {{resources}} resources, {{articles}} articles.", + "generateError": "Error generating data", + "clearError": "Error clearing data", + "expires": "Expires: {{date}}", + "guests": "{{count}} guests" + }, + "events": { + "title": "Events", + "newEvent": "New Event", + "code": "Code", + "date": "Date", + "description": "Description", + "client": "Client", + "location": "Location", + "guests": "Guests", + "status": "Status", + "type": "Event Type", + "eventDate": "Event Date", + "deleteSuccess": "Event deleted successfully", + "detail": { + "status": { + "draft": "Draft", + "quote": "Quote", + "confirmed": "Confirmed", + "new": "New" + }, + "loading": "Loading...", + "newEvent": "New Event", + "noDescription": "No description", + "actions": { + "duplicate": "Duplicate", + "recalculate": "Recalculate Qty", + "confirm": "Confirm", + "save": "Save", + "print": "Print PDF", + "back": "Back" + }, + "fields": { + "date": "Event Date", + "startTime": "Start Time", + "endTime": "End Time", + "type": "Event Type", + "description": "Event Description", + "descriptionPlaceholder": "e.g. Wedding Smith-Jones", + "client": "Client", + "location": "Location", + "totalGuests": "Total Guests", + "costPerPerson": "Cost per Person", + "totalCost": "Total Cost", + "totalDeposits": "Total Deposits", + "balance": "Balance", + "status": "Status" + }, + "tabs": { + "guests": "Guests", + "withdrawalList": "Withdrawal List", + "resources": "Resources", + "costs": "Costs", + "notes": "Notes" + }, + "guestsTab": { + "total": "Total guests", + "add": "Add Guest Type", + "type": "Guest Type", + "quantity": "Quantity", + "notes": "Notes", + "empty": "No guests added. Click \"Add Guest Type\" to start." + }, + "withdrawalTab": { + "total": "Items in list", + "add": "Add Item", + "code": "Code", + "article": "Article", + "qtyRequested": "Qty Requested", + "qtyCalculated": "Qty Calculated", + "qtyActual": "Qty Actual", + "notes": "Notes", + "empty": "No items in list." + }, + "resourcesTab": { + "total": "Committed resources", + "add": "Add Resource", + "resource": "Resource", + "quantity": "Quantity", + "costUnit": "Unit Cost", + "costTotal": "Total Cost", + "notes": "Notes", + "empty": "No resources added." + }, + "dialogs": { + "addGuest": "Add Guest", + "addArticle": "Add Item", + "addResource": "Add Resource", + "cancel": "Cancel", + "add": "Add" + } + } + }, + "clients": { + "title": "Clients", + "newClient": "New Client", + "editClient": "Edit Client", + "code": "Code", + "altCode": "Alt. Code", + "businessName": "Business Name", + "city": "City", + "province": "Prov.", + "phone": "Phone", + "email": "Email", + "vat": "VAT", + "address": "Address", + "zip": "ZIP", + "pec": "PEC", + "fiscalCode": "Fiscal Code", + "recipientCode": "Recipient Code", + "generatedOnSave": "(Generated on save)", + "autoGenerated": "Automatically generated", + "willBeAssigned": "Will be assigned automatically", + "deleteConfirm": "Delete this client?" + }, + "location": { + "title": "Location", + "newLocation": "New Location", + "editLocation": "Edit Location", + "name": "Name", + "city": "City", + "province": "Prov.", + "distance": "Distance (km)", + "contact": "Contact", + "phone": "Phone", + "address": "Address", + "zip": "ZIP", + "email": "Email", + "deleteConfirm": "Delete this location?" + }, + "articles": { + "title": "Articles", + "newArticle": "New Article", + "editArticle": "Edit Article", + "code": "Code", + "altCode": "Alt. Code", + "description": "Description", + "type": "Type", + "category": "Category", + "available": "Available", + "qtyA": "Qty A", + "qtyB": "Qty B", + "qtyS": "Qty S", + "uom": "UOM", + "qtyAvailable": "Quantity Available", + "unitOfMeasure": "Unit of Measure", + "qtyStdAdults": "Std Qty Adults (A)", + "qtyStdBuffet": "Std Qty Buffet (B)", + "qtyStdSeated": "Std Qty Seated (S)", + "generatedOnSave": "(Generated on save)", + "autoGenerated": "Automatically generated", + "willBeAssigned": "Will be assigned automatically", + "deleteConfirm": "Delete this article?", + "materialType": "Material Type" + }, + "resources": { + "title": "Resources", + "newResource": "New Resource", + "editResource": "Edit Resource", + "name": "Name", + "surname": "Surname", + "type": "Type", + "phone": "Phone", + "email": "Email", + "resourceType": "Resource Type", + "deleteConfirm": "Delete this resource?" + }, + "calendar": { + "title": "Events Calendar", + "newEvent": "New Event", + "createEvent": "Create Event", + "createEventConfirm": "Do you want to create a new event for", + "today": "Today", + "month": "Month", + "week": "Week", + "day": "Day" + }, + "status": { + "scheda": "Draft", + "preventivo": "Quote", + "confermato": "Confirmed" + }, + "modules": { + "warehouse": { + "title": "Warehouse Management", + "inventory": "Inventory", + "movements": "Movements", + "stock": "Stock", + "categories": "Categories" + }, + "admin": { + "title": "Module Management", + "subtitle": "Configure active modules and manage subscriptions", + "checkExpired": "Check Expired", + "refresh": "Refresh", + "expiringWarning": "{{count}} module(s) expiring in the next 30 days:", + "disableConfirmTitle": "Confirm Deactivation", + "disableConfirmText": "Are you sure you want to deactivate the module", + "disableConfirmSubtext": "Entered data will remain in the system but will not be accessible until reactivation.", + "disable": "Deactivate", + "enable": "Activate", + "details": "Details", + "renew": "Renew", + "active": "Active", + "inactive": "Inactive", + "core": "Core", + "annualPrice": "Annual Price", + "monthlyPrice": "Monthly Price", + "dependencies": "Dependencies", + "subscriptionDetails": "Subscription Details", + "type": "Type", + "startDate": "Start Date", + "endDate": "End Date", + "daysRemaining": "Days Remaining", + "autoRenew": "Auto Renew", + "yes": "Yes", + "no": "No", + "purchaseTitle": "Activate Module", + "purchaseSubtitle": "Choose the subscription plan for the module {{name}}", + "missingDependencies": "This module requires the following modules which are not active:", + "subscriptionType": "Subscription Type", + "monthly": "Monthly", + "annual": "Annual", + "perMonth": "/month", + "perYear": "/year", + "savings": "Save {{percent}}%", + "autoRenewLabel": "Auto renew at expiration", + "orderSummary": "Order Summary", + "total": "Total", + "activating": "Activating...", + "activateModule": "Activate Module", + "purchaseNote": "You can deactivate the module at any time from the settings. Entered data will remain available.", + "includedFeatures": "Included Features", + "moduleNotFound": "Module Not Found", + "moduleNotFoundText": "The requested module does not exist.", + "backToHome": "Back to Home", + "status": "Status", + "module": "Module", + "subscription": "Subscription", + "activationError": "Error during module activation" + }, + "features": { + "warehouse": { + "0": "Article master data management", + "1": "Warehouse movements (inbound/outbound)", + "2": "Real-time stock levels", + "3": "Stock valuation (FIFO, LIFO, weighted average)", + "4": "Inventory and adjustments", + "5": "Stock and movement reports" + }, + "purchases": { + "0": "Supplier order management", + "1": "Inbound delivery notes", + "2": "Purchase invoices", + "3": "Payment schedule", + "4": "Purchase analysis by supplier/article", + "5": "Purchase price history" + }, + "sales": { + "0": "Customer order management", + "1": "Outbound delivery notes", + "2": "Electronic invoicing", + "3": "Collection schedule", + "4": "Sales analysis by customer/article", + "5": "Price lists" + }, + "production": { + "0": "Multi-level bills of materials", + "1": "Work cycles", + "2": "Production orders", + "3": "MRP planning", + "4": "Production progress", + "5": "Production costs" + }, + "quality": { + "0": "Control plans", + "1": "Control recording", + "2": "Non-conformity management", + "3": "Corrective/preventive actions", + "4": "Certifications and audits", + "5": "Quality statistics" + }, + "default": "Complete module features" + } + }, + "autoCodes": { + "title": "Automatic Codes", + "subtitle": "Configure patterns for automatic code generation", + "helpPattern": "Pattern Guide", + "entity": "Entity", + "prefix": "Prefix", + "pattern": "Pattern", + "example": "Example", + "sequence": "Sequence", + "reset": "Reset", + "status": "Status", + "monthly": "Monthly", + "yearly": "Yearly", + "never": "Never", + "previewTooltip": "Preview next code", + "resetTooltip": "Reset sequence", + "resetConfirmTitle": "Confirm Sequence Reset", + "resetConfirmText": "Are you sure you want to reset the sequence for", + "resetWarning": "The sequence will be reset to 0. The next generated code will start from 1.", + "previewTitle": "Next Code Preview", + "previewText": "This is the code that will be generated upon next creation.\nThe sequence has not been incremented.", + "helpTitle": "Pattern Guide", + "helpText": "Patterns define how automatic codes are generated. You can combine static text and dynamic placeholders.", + "placeholders": "Available Placeholders", + "examples": "Pattern Examples", + "editTitle": "Edit Configuration", + "prefixHelper": "Text replaced in {PREFIX} placeholder", + "patternHelper": "Pattern for code generation", + "previewLabel": "Preview:", + "resetSequence": "Reset Sequence", + "everyYear": "Every year", + "everyMonth": "Every month", + "generationActive": "Generation active", + "readOnly": "Code not editable" + }, + "customFields": { + "title": "Custom Fields Management", + "sectionTitle": "Custom Fields", + "entity": "Entity", + "newField": "New Field", + "label": "Label", + "fieldName": "Internal Name", + "type": "Type", + "required": "Required", + "order": "Order", + "editField": "Edit Field", + "deleteConfirm": "Are you sure you want to delete this field?", + "fieldNameHelper": "Must be unique for the entity. Use only lowercase letters and underscores.", + "optionsJson": "Options (JSON Array)", + "optionsHelper": "Enter a valid JSON array of strings", + "description": "Description / Helper Text", + "noFields": "No custom fields configured for this entity.", + "types": { + "text": "Text", + "number": "Number", + "date": "Date", + "boolean": "Boolean (Yes/No)", + "select": "Dropdown List", + "multiselect": "Multi-Select", + "textarea": "Text Area", + "color": "Color", + "url": "URL", + "email": "Email" + }, + "entities": { + "client": "Clients", + "article": "Articles (Catering)", + "event": "Events", + "warehousearticle": "Warehouse Articles", + "warehouselocation": "Warehouses", + "resource": "Resources (Staff)" + } + }, + "reports": { + "title": "Report Templates", + "import": "Import", + "newTemplate": "New Template", + "filterCategory": "Filter by category", + "all": "All", + "importTemplate": "Import Template", + "noTemplates": "No templates found", + "createFirstTemplate": "Create your first report template or import an existing one", + "createTemplate": "Create Template", + "vertical": "Portrait", + "horizontal": "Landscape", + "edit": "Edit", + "duplicate": "Duplicate", + "export": "Export", + "delete": "Delete", + "confirmDelete": "Confirm Deletion", + "deleteConfirmText": "Are you sure you want to delete the template \"{{name}}\"?", + "irreversibleAction": "This action cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting...", + "importTitle": "Import Template", + "importText": "Select an .aprt file to import", + "selectFile": "Select File", + "importing": "Importing...", + "categories": { + "Evento": "Event", + "Cliente": "Client", + "Articoli": "Articles", + "Generale": "General", + "Importato": "Imported" + }, + "editor": { + "newTemplate": "New Template", + "templateUpdatedByOther": "Template updated by another user", + "saveSuccess": "Template saved successfully", + "saveError": "Error saving: {{error}}", + "pageName": "Page {{number}}", + "copyOf": "{{name}} (copy)", + "newText": "New text", + "column": "Column {{number}}", + "elementCopied": "Element copied", + "pastedSuffix": "_pasted", + "copySuffix": "_copy", + "groupingNotImplemented": "Grouping not yet implemented", + "ungroupingNotImplemented": "Ungrouping not yet implemented", + "doubleClickToEdit": "Double click text to edit", + "fitToContentNotImplemented": "Fit to content not yet implemented", + "selectDatasetForPreview": "Select at least one dataset for preview", + "saveBeforePreview": "Save the template before previewing", + "previewError": "Error generating preview: {{error}}", + "panels": { + "pages": "Pages", + "data": "Data Fields", + "properties": "Properties" + }, + "defaultPageName": "Page 1", + "saveDialog": { + "title": "Save Template", + "name": "Name", + "description": "Description", + "category": "Category", + "cancel": "Cancel", + "saving": "Saving...", + "save": "Save" + } + } + }, + "warehouse": { + "dashboard": { + "newInbound": "New Inbound", + "stockLevels": "Stock Levels", + "activeArticles": "Active Articles", + "warehouses": "Warehouses", + "totalValue": "Total Value", + "lowStock": "Low Stock", + "outOfStock": "out of stock", + "recentMovements": "Recent Movements", + "viewAll": "View All", + "noRecentMovements": "No recent movements", + "lines": "lines", + "manage": "Manage", + "draftMovements": "draft movements to confirm", + "view": "View", + "expiringBatches": "batches expiring in the next 30 days", + "lowStockArticles": "Low Stock Articles", + "noLowStockArticles": "No low stock articles", + "quickActions": "Quick Actions", + "inbound": "Inbound", + "outbound": "Outbound", + "transfer": "Transfer", + "newArticle": "New Article", + "inventory": "Inventory", + "valuation": "Valuation" + }, + "articles": { + "title": "Articles Registry", + "newArticle": "New Article", + "columns": { + "code": "Code", + "description": "Description", + "category": "Category", + "uom": "U.O.M.", + "averageCost": "Average Cost", + "status": "Status", + "active": "Active", + "inactive": "Inactive" + }, + "filters": { + "searchPlaceholder": "Search by code or description...", + "category": "Category", + "all": "All", + "showAll": "Show All", + "onlyActive": "Active Only", + "viewList": "List View", + "viewGrid": "Grid View" + }, + "loadingError": "Error loading articles: {{error}}", + "noArticlesFound": "No articles found", + "actions": { + "edit": "Edit", + "viewStock": "View Stock", + "delete": "Delete" + }, + "deleteDialog": { + "title": "Confirm Deletion", + "content": "Are you sure you want to delete article {{code}} - {{description}}?", + "warning": "This action cannot be undone.", + "cancel": "Cancel", + "deleting": "Deleting...", + "delete": "Delete" + } + }, + "articleForm": { + "titleNew": "New Article", + "titleEdit": "Article: {{code}}", + "tabs": { + "general": "General Data", + "stock": "Stock", + "batches": "Batches", + "serials": "Serials" + }, + "sections": { + "basicInfo": "Basic Information", + "stockLevels": "Stock Levels", + "costs": "Costs and Valuation", + "traceability": "Traceability", + "image": "Image", + "summary": "Summary" + }, + "fields": { + "code": "Code", + "alternativeCode": "Alternative Code", + "description": "Description", + "shortDescription": "Short Description", + "category": "Category", + "uom": "Unit of Measure", + "barcode": "Barcode", + "notes": "Notes", + "minStock": "Minimum Stock", + "maxStock": "Maximum Stock", + "reorderPoint": "Reorder Point", + "reorderQuantity": "Reorder Quantity", + "standardCost": "Standard Cost", + "stockManagement": "Stock Management", + "valuationMethod": "Valuation Method", + "batchManaged": "Batch Management", + "serialManaged": "Serial Management", + "expiryManaged": "Expiry Management", + "active": "Active Article" + }, + "helpers": { + "generatedOnSave": "(Generated on save)", + "willBeGenerated": "Will be assigned automatically", + "generatedAutomatically": "Generated automatically", + "optional": "Optional" + }, + "validation": { + "codeRequired": "Code is required", + "descriptionRequired": "Description is required", + "uomRequired": "Unit of measure is required" + }, + "errors": { + "saveError": "Error saving: {{error}}" + }, + "actions": { + "upload": "Upload", + "cancel": "Cancel", + "save": "Save", + "saving": "Saving..." + }, + "summary": { + "averageCost": "Average Cost", + "lastPurchase": "Last Purchase" + }, + "tables": { + "warehouse": "Warehouse", + "quantity": "Quantity", + "reserved": "Reserved", + "available": "Available", + "value": "Value", + "batchNumber": "Batch Number", + "expiryDate": "Expiry Date", + "status": "Status", + "serialNumber": "Serial Number", + "lot": "Lot", + "noStock": "No stock", + "noBatches": "No batches", + "noSerials": "No serials" + }, + "status": { + "expired": "Expired", + "available": "Available", + "unavailable": "Unavailable" + }, + "options": { + "noCategory": "None" + } + }, + "stockManagementType": { + "Standard": "Standard", + "NotManaged": "Not Managed", + "VariableWeight": "Variable Weight", + "Kit": "Kit" + }, + "valuationMethod": { + "WeightedAverage": "Weighted Average", + "FIFO": "FIFO", + "LIFO": "LIFO", + "StandardCost": "Standard Cost", + "SpecificCost": "Specific Cost" + }, + "movements": { + "title": "Warehouse Movements", + "filters": { + "searchPlaceholder": "Search document, reference...", + "warehouse": "Warehouse", + "all": "All", + "type": "Type", + "status": "Status", + "from": "From", + "to": "To", + "reset": "Reset" + }, + "columns": { + "document": "Document", + "date": "Date", + "type": "Type", + "status": "Status", + "warehouse": "Warehouse", + "destination": "Destination", + "reason": "Reason", + "lines": "Lines", + "value": "Value", + "reference": "Reference" + }, + "actions": { + "newMovement": "New Movement", + "inbound": "Inbound", + "outbound": "Outbound", + "transfer": "Transfer", + "adjustment": "Adjustment", + "view": "View", + "confirm": "Confirm", + "cancel": "Cancel", + "delete": "Delete" + }, + "dialogs": { + "confirm": { + "title": "Confirm Movement", + "content": "Confirm movement {{doc}}?", + "warning": "Stock levels will be updated and the movement cannot be modified anymore.", + "confirming": "Confirming...", + "confirm": "Confirm", + "cancel": "Cancel" + }, + "cancel": { + "title": "Cancel Movement", + "content": "Cancel movement {{doc}}?", + "warning": "The movement will be marked as cancelled but not deleted.", + "cancelling": "Cancelling...", + "cancelMovement": "Cancel Movement", + "back": "Back" + }, + "delete": { + "title": "Delete Movement", + "content": "Permanently delete movement {{doc}}?", + "warning": "This action cannot be undone.", + "deleting": "Deleting...", + "delete": "Delete", + "cancel": "Cancel" + } + }, + "loadingError": "Error loading movements: {{error}}" + }, + "movementType": { + "Inbound": "Inbound", + "Outbound": "Outbound", + "Transfer": "Transfer", + "Adjustment": "Adjustment", + "Production": "Production", + "Consumption": "Consumption", + "SupplierReturn": "Supplier Return", + "CustomerReturn": "Customer Return" + }, + "movementStatus": { + "Draft": "Draft", + "Confirmed": "Confirmed", + "Cancelled": "Cancelled" + }, + "inbound": { + "title": "New Inbound", + "subtitle": "Goods receipt movement", + "sections": { + "movementData": "Movement Data", + "lines": "Movement Lines" + }, + "fields": { + "date": "Movement Date", + "warehouse": "Warehouse", + "documentNumber": "Document Number", + "externalReference": "External Reference", + "notes": "Notes", + "article": "Article", + "quantity": "Quantity", + "unitCost": "Unit Cost", + "total": "Total" + }, + "placeholders": { + "documentNumber": "Delivery Note, Invoice, etc.", + "externalReference": "Order, Supplier, etc.", + "selectArticle": "Select article" + }, + "actions": { + "addLine": "Add Line", + "cancel": "Cancel", + "saveDraft": "Save Draft", + "saveAndConfirm": "Save and Confirm" + }, + "totals": { + "quantity": "Total Quantity", + "value": "Total Value" + }, + "validation": { + "warehouseRequired": "Select a warehouse", + "dateRequired": "Enter date", + "linesRequired": "Enter at least one line with article and quantity" + }, + "errors": { + "saveError": "Error: {{error}}" + } + }, + "outbound": { + "title": "New Outbound", + "subtitle": "Goods issue movement", + "warnings": { + "stockIssues": "Warning: some lines exceed available stock", + "overStock": "Quantity exceeds availability" + }, + "sections": { + "movementData": "Movement Data", + "lines": "Movement Lines" + }, + "fields": { + "date": "Movement Date", + "warehouse": "Warehouse", + "documentNumber": "Document Number", + "externalReference": "External Reference", + "notes": "Notes", + "article": "Article", + "available": "Available", + "quantity": "Quantity" + }, + "placeholders": { + "documentNumber": "Delivery Note, etc.", + "externalReference": "Order, Customer, etc.", + "selectArticle": "Select article", + "notes": "Notes" + }, + "actions": { + "addLine": "Add Line", + "cancel": "Cancel", + "saveDraft": "Save Draft", + "saveAndConfirm": "Save and Confirm" + }, + "totals": { + "quantity": "Total Quantity" + }, + "validation": { + "warehouseRequired": "Select a warehouse", + "dateRequired": "Enter date", + "linesRequired": "Enter at least one line with article and quantity" + }, + "errors": { + "saveError": "Error: {{error}}" + } + }, + "transfer": { + "title": "Warehouse Transfer", + "subtitle": "Move goods between warehouses", + "sections": { + "transferData": "Transfer Data", + "lines": "Items to Transfer" + }, + "fields": { + "date": "Date", + "sourceWarehouse": "Source Warehouse", + "destWarehouse": "Destination Warehouse", + "document": "Document", + "externalReference": "External Reference", + "notes": "Notes", + "article": "Article", + "available": "Available", + "quantity": "Quantity" + }, + "placeholders": { + "article": "Article", + "notes": "Notes" + }, + "actions": { + "add": "Add", + "cancel": "Cancel", + "saveDraft": "Save Draft", + "saveAndConfirm": "Save and Confirm" + }, + "totals": { + "total": "Total: {{value}}" + }, + "validation": { + "sourceRequired": "Select source warehouse", + "destRequired": "Select destination warehouse", + "sameWarehouse": "Source and destination must be different", + "dateRequired": "Enter date", + "linesRequired": "Enter at least one line" + }, + "errors": { + "saveError": "Error: {{error}}" + } + }, + "locations": { + "title": "Warehouse Management", + "newWarehouse": "New Warehouse", + "emptyState": { + "title": "No warehouses configured", + "action": "Add the first warehouse" + }, + "card": { + "default": "Default Warehouse", + "inactive": "Inactive", + "setDefault": "Set as default", + "edit": "Edit", + "delete": "Delete" + }, + "dialog": { + "createTitle": "New Warehouse", + "editTitle": "Edit Warehouse", + "fields": { + "code": "Code", + "alternativeCode": "Alternative Code", + "name": "Name", + "description": "Description", + "type": "Type", + "address": "Address", + "isDefault": "Default Warehouse", + "isActive": "Active" + }, + "helpers": { + "generatedOnSave": "(Generated on save)", + "generatedAutomatically": "Generated automatically", + "willBeAssigned": "Will be assigned automatically", + "optional": "Optional" + }, + "validation": { + "nameRequired": "Name is required" + }, + "actions": { + "cancel": "Cancel", + "save": "Save", + "saving": "Saving..." + } + }, + "deleteDialog": { + "title": "Confirm Deletion", + "content": "Are you sure you want to delete warehouse {{code}} - {{name}}?", + "warning": "This action cannot be undone.", + "deleting": "Deleting...", + "delete": "Delete", + "cancel": "Cancel" + }, + "loadingError": "Error loading warehouses: {{error}}" + }, + "warehouseType": { + "Physical": "Physical", + "Transit": "Transit", + "Returns": "Returns", + "Defective": "Defective", + "Subcontract": "Subcontract" + }, + "stockLevels": { + "title": "Stock Levels", + "valuation": "Valuation", + "summary": { + "articles": "Articles", + "totalQuantity": "Total Quantity", + "totalValue": "Total Value", + "lowStock": "Low Stock" + }, + "filters": { + "search": "Search article...", + "warehouse": "Warehouse", + "category": "Category", + "lowStockOnly": "Low stock only", + "allWarehouses": "All", + "allCategories": "All" + }, + "columns": { + "code": "Code", + "article": "Article", + "warehouse": "Warehouse", + "category": "Category", + "quantity": "Quantity", + "reserved": "Reserved", + "available": "Available", + "averageCost": "Average Cost", + "value": "Value" + }, + "error": "Error: {{error}}" + }, + "inventory": { + "title": "Physical Inventory", + "newInventory": "New Inventory", + "status": { + "Draft": "Draft", + "InProgress": "In Progress", + "Completed": "Completed", + "Confirmed": "Confirmed", + "Cancelled": "Cancelled" + }, + "columns": { + "code": "Code", + "description": "Description", + "date": "Inventory Date", + "warehouse": "Warehouse", + "category": "Category", + "status": "Status", + "progress": "Progress", + "actions": "Actions" + }, + "actions": { + "view": "Details", + "start": "Start Counting", + "continue": "Continue Counting", + "cancel": "Cancel" + }, + "confirmCancel": "Are you sure you want to cancel this inventory?", + "form": { + "title": { + "new": "New Inventory", + "edit": "Edit Inventory", + "editWithCode": "Inventory {{code}}" + }, + "breadcrumbs": { + "list": "Inventory" + }, + "fields": { + "description": "Description", + "date": "Inventory Date", + "warehouse": "Warehouse", + "category": "Category (Optional)", + "type": "Inventory Type", + "notes": "Notes" + }, + "options": { + "allCategories": "All", + "type": { + "Full": "Full", + "Partial": "Partial", + "Cyclic": "Cyclic", + "Sample": "Sample" + } + }, + "actions": { + "back": "Back", + "save": "Save Changes", + "create": "Create and Start" + } + }, + "count": { + "title": "Inventory: {{description}}", + "actions": { + "back": "Back", + "start": "Start Inventory", + "complete": "Complete Count", + "confirm": "Confirm and Adjust" + }, + "cards": { + "date": "Inventory Date", + "warehouse": "Warehouse", + "totalLines": "Total Lines", + "countedLines": "Counted Lines" + }, + "alert": { + "completed": "Inventory is completed. Verify differences before confirming. Confirmation will automatically generate adjustment movements." + }, + "columns": { + "articleCode": "Article Code", + "description": "Description", + "batch": "Batch", + "location": "Location", + "theoreticalQty": "Theoretical Qty", + "countedQty": "Counted Qty", + "difference": "Difference" + }, + "confirmDialog": { + "title": "Confirm Inventory", + "content": "Are you sure you want to confirm the inventory? This operation is irreversible and will generate adjustment movements for any differences found.", + "cancel": "Cancel", + "confirm": "Confirm" + } + } + } + } +} \ No newline at end of file diff --git a/frontend/public/locales/it/translation.json b/frontend/public/locales/it/translation.json new file mode 100644 index 0000000..c1f9a52 --- /dev/null +++ b/frontend/public/locales/it/translation.json @@ -0,0 +1,1128 @@ +{ + "common": { + "settings": "Impostazioni", + "theme": "Tema", + "language": "Lingua", + "dark": "Scuro", + "light": "Chiaro", + "logout": "Esci", + "save": "Salva", + "cancel": "Annulla", + "close": "Chiudi", + "delete": "Elimina", + "edit": "Modifica", + "new": "Nuovo", + "search": "Cerca", + "actions": "Azioni", + "confirm": "Conferma", + "back": "Indietro", + "loading": "Caricamento...", + "error": "Errore", + "success": "Successo", + "unknown": "Sconosciuto", + "deleteAll": "Elimina Tutto", + "generate": "Genera", + "warning": "Attenzione", + "create": "Crea", + "deleteConfirm": "Eliminare questo elemento?", + "optional": "Opzionale", + "notes": "Note", + "preview": "Anteprima", + "none": "Nessuno" + }, + "menu": { + "dashboard": "Dashboard", + "calendar": "Calendario", + "events": "Eventi", + "clients": "Clienti", + "location": "Location", + "articles": "Articoli", + "resources": "Risorse", + "warehouse": "Magazzino", + "reports": "Report", + "modules": "Moduli", + "autoCodes": "Codici Auto", + "customFields": "Campi Personalizzati" + }, + "dashboard": { + "title": "Dashboard", + "totalEvents": "Eventi Totali", + "confirmed": "Confermati", + "inQuote": "In Preventivo", + "eventsToday": "Eventi Oggi", + "upcomingEvents": "Prossimi Eventi (30 giorni)", + "expiringQuotes": "Preventivi in Scadenza", + "noEvents": "Nessun evento nei prossimi 30 giorni", + "noQuotes": "Nessun preventivo in attesa", + "generateDemoData": "Genera Dati Demo", + "clearDatabase": "Pulisci Database", + "generateDialogTitle": "Genera Dati Demo", + "generateDialogText": "Questa operazione genera dati di test per dimostrazioni:
- 15 Clienti
- 10 Location
- 12 Risorse (staff)
- 20 Articoli
- 20 Eventi con dettagli

I dati esistenti non verranno modificati.", + "clearDialogTitle": "Pulisci Database", + "clearDialogWarning": "Attenzione: questa operazione elimina TUTTI i dati dal database!", + "clearDialogText": "Verranno eliminati:
- Tutti gli eventi e i relativi dettagli
- Tutti i clienti
- Tutte le location
- Tutte le risorse
- Tutti gli articoli

Questa operazione non puo essere annullata.", + "clearSuccess": "Database pulito. Eliminati: {{events}} eventi, {{clients}} clienti, {{locations}} location, {{resources}} risorse, {{articles}} articoli.", + "generateError": "Errore durante la generazione dei dati", + "clearError": "Errore durante la pulizia dei dati", + "expires": "Scade: {{date}}", + "guests": "{{count}} ospiti" + }, + "events": { + "title": "Eventi", + "newEvent": "Nuovo Evento", + "code": "Codice", + "date": "Data", + "description": "Descrizione", + "client": "Cliente", + "location": "Location", + "guests": "Ospiti", + "status": "Stato", + "type": "Tipo Evento", + "eventDate": "Data Evento", + "deleteSuccess": "Evento eliminato con successo", + "detail": { + "status": { + "draft": "Scheda Evento", + "quote": "Preventivo", + "confirmed": "Confermato", + "new": "Nuovo" + }, + "loading": "Caricamento...", + "newEvent": "Nuovo Evento", + "noDescription": "Senza descrizione", + "actions": { + "duplicate": "Duplica", + "recalculate": "Ricalcola Qta", + "confirm": "Conferma", + "save": "Salva", + "print": "Stampa PDF", + "back": "Indietro" + }, + "fields": { + "date": "Data Evento", + "startTime": "Ora Inizio", + "endTime": "Ora Fine", + "type": "Tipo Evento", + "description": "Descrizione Evento", + "descriptionPlaceholder": "es. Matrimonio Rossi-Bianchi", + "client": "Cliente", + "location": "Location", + "totalGuests": "N. Ospiti Totale", + "costPerPerson": "Costo a Persona", + "totalCost": "Costo Totale", + "totalDeposits": "Totale Acconti", + "balance": "Saldo", + "status": "Stato" + }, + "tabs": { + "guests": "Ospiti", + "withdrawalList": "Lista Prelievo", + "resources": "Risorse", + "costs": "Costi", + "notes": "Note" + }, + "guestsTab": { + "total": "Totale ospiti", + "add": "Aggiungi Tipo Ospite", + "type": "Tipo Ospite", + "quantity": "Quantità", + "notes": "Note", + "empty": "Nessun ospite aggiunto. Clicca \"Aggiungi Tipo Ospite\" per iniziare." + }, + "withdrawalTab": { + "total": "Articoli in lista", + "add": "Aggiungi Articolo", + "code": "Codice", + "article": "Articolo", + "qtyRequested": "Qta Richiesta", + "qtyCalculated": "Qta Calcolata", + "qtyActual": "Qta Effettiva", + "notes": "Note", + "empty": "Nessun articolo in lista." + }, + "resourcesTab": { + "total": "Risorse impegnate", + "add": "Aggiungi Risorsa", + "resource": "Risorsa", + "quantity": "Quantità", + "costUnit": "Costo Unitario", + "costTotal": "Costo Totale", + "notes": "Note", + "empty": "Nessuna risorsa aggiunta." + }, + "dialogs": { + "addGuest": "Aggiungi Ospite", + "addArticle": "Aggiungi Articolo", + "addResource": "Aggiungi Risorsa", + "cancel": "Annulla", + "add": "Aggiungi" + } + } + }, + "clients": { + "title": "Clienti", + "newClient": "Nuovo Cliente", + "editClient": "Modifica Cliente", + "code": "Codice", + "altCode": "Cod. Alt.", + "businessName": "Ragione Sociale", + "city": "Città", + "province": "Prov.", + "phone": "Telefono", + "email": "Email", + "vat": "P.IVA", + "address": "Indirizzo", + "zip": "CAP", + "pec": "PEC", + "fiscalCode": "Codice Fiscale", + "recipientCode": "Codice Destinatario", + "generatedOnSave": "(Generato al salvataggio)", + "autoGenerated": "Generato automaticamente", + "willBeAssigned": "Verrà assegnato automaticamente", + "deleteConfirm": "Eliminare questo cliente?" + }, + "location": { + "title": "Location", + "newLocation": "Nuova Location", + "editLocation": "Modifica Location", + "name": "Nome", + "city": "Città", + "province": "Prov.", + "distance": "Distanza (km)", + "contact": "Referente", + "phone": "Telefono", + "address": "Indirizzo", + "zip": "CAP", + "email": "Email", + "deleteConfirm": "Eliminare questa location?" + }, + "articles": { + "title": "Articoli", + "newArticle": "Nuovo Articolo", + "editArticle": "Modifica Articolo", + "code": "Codice", + "altCode": "Cod. Alt.", + "description": "Descrizione", + "type": "Tipo", + "category": "Categoria", + "available": "Disponibile", + "qtyA": "Qta A", + "qtyB": "Qta B", + "qtyS": "Qta S", + "uom": "UM", + "qtyAvailable": "Quantità Disponibile", + "unitOfMeasure": "Unità Misura", + "qtyStdAdults": "Qta Std Adulti (A)", + "qtyStdBuffet": "Qta Std Buffet (B)", + "qtyStdSeated": "Qta Std Seduti (S)", + "generatedOnSave": "(Generato al salvataggio)", + "autoGenerated": "Generato automaticamente", + "willBeAssigned": "Verrà assegnato automaticamente", + "deleteConfirm": "Eliminare questo articolo?", + "materialType": "Tipo Materiale" + }, + "resources": { + "title": "Risorse", + "newResource": "Nuova Risorsa", + "editResource": "Modifica Risorsa", + "name": "Nome", + "surname": "Cognome", + "type": "Tipo", + "phone": "Telefono", + "email": "Email", + "resourceType": "Tipo Risorsa", + "deleteConfirm": "Eliminare questa risorsa?" + }, + "calendar": { + "title": "Calendario Eventi", + "newEvent": "Nuovo Evento", + "createEvent": "Crea Evento", + "createEventConfirm": "Vuoi creare un nuovo evento per il giorno", + "today": "Oggi", + "month": "Mese", + "week": "Settimana", + "day": "Giorno" + }, + "status": { + "scheda": "Scheda", + "preventivo": "Preventivo", + "confermato": "Confermato" + }, + "modules": { + "warehouse": { + "title": "Gestione Magazzino", + "inventory": "Inventario", + "movements": "Movimenti", + "stock": "Giacenze", + "categories": "Categorie" + }, + "admin": { + "title": "Gestione Moduli", + "subtitle": "Configura i moduli attivi e gestisci le subscription", + "checkExpired": "Controlla Scadenze", + "refresh": "Aggiorna", + "expiringWarning": "{{count}} modulo/i in scadenza nei prossimi 30 giorni:", + "disableConfirmTitle": "Conferma disattivazione", + "disableConfirmText": "Sei sicuro di voler disattivare il modulo", + "disableConfirmSubtext": "I dati inseriti rimarranno nel sistema ma non saranno più accessibili fino alla riattivazione.", + "disable": "Disattiva", + "enable": "Attiva", + "details": "Dettagli", + "renew": "Rinnova", + "active": "Attivo", + "inactive": "Disattivo", + "core": "Core", + "annualPrice": "Prezzo annuale", + "monthlyPrice": "Prezzo mensile", + "dependencies": "Dipendenze", + "subscriptionDetails": "Dettagli Subscription", + "type": "Tipo", + "startDate": "Data inizio", + "endDate": "Data scadenza", + "daysRemaining": "Giorni rimanenti", + "autoRenew": "Rinnova automatico", + "yes": "Sì", + "no": "No", + "purchaseTitle": "Attiva Modulo", + "purchaseSubtitle": "Scegli il piano di abbonamento per il modulo {{name}}", + "missingDependencies": "Questo modulo richiede i seguenti moduli che non sono attivi:", + "subscriptionType": "Tipo di abbonamento", + "monthly": "Mensile", + "annual": "Annuale", + "perMonth": "/mese", + "perYear": "/anno", + "savings": "Risparmi {{percent}}%", + "autoRenewLabel": "Rinnova automatico alla scadenza", + "orderSummary": "Riepilogo ordine", + "total": "Totale", + "activating": "Attivazione in corso...", + "activateModule": "Attiva Modulo", + "purchaseNote": "Potrai disattivare il modulo in qualsiasi momento dalle impostazioni. I dati inseriti rimarranno disponibili.", + "includedFeatures": "Funzionalità incluse", + "moduleNotFound": "Modulo non trovato", + "moduleNotFoundText": "Il modulo richiesto non esiste.", + "backToHome": "Torna alla Home", + "status": "Stato", + "module": "Modulo", + "subscription": "Abbonamento", + "activationError": "Errore durante l'attivazione del modulo" + }, + "features": { + "warehouse": { + "0": "Gestione anagrafica articoli", + "1": "Movimenti di magazzino (carico/scarico)", + "2": "Giacenze in tempo reale", + "3": "Valorizzazione scorte (FIFO, LIFO, medio ponderato)", + "4": "Inventario e rettifiche", + "5": "Report giacenze e movimenti" + }, + "purchases": { + "0": "Gestione ordini a fornitore", + "1": "DDT di entrata", + "2": "Fatture passive", + "3": "Scadenziario pagamenti", + "4": "Analisi acquisti per fornitore/articolo", + "5": "Storico prezzi di acquisto" + }, + "sales": { + "0": "Gestione ordini cliente", + "1": "DDT di uscita", + "2": "Fatturazione elettronica", + "3": "Scadenziario incassi", + "4": "Analisi vendite per cliente/articolo", + "5": "Listini prezzi" + }, + "production": { + "0": "Distinte base multilivello", + "1": "Cicli di lavoro", + "2": "Ordini di produzione", + "3": "Pianificazione MRP", + "4": "Avanzamento produzione", + "5": "Costi di produzione" + }, + "quality": { + "0": "Piani di controllo", + "1": "Registrazione controlli", + "2": "Gestione non conformità", + "3": "Azioni correttive/preventive", + "4": "Certificazioni e audit", + "5": "Statistiche qualità" + }, + "default": "Funzionalità complete del modulo" + } + }, + "autoCodes": { + "title": "Codici Automatici", + "subtitle": "Configura i pattern per la generazione automatica dei codici", + "helpPattern": "Guida Pattern", + "entity": "Entità", + "prefix": "Prefisso", + "pattern": "Pattern", + "example": "Esempio", + "sequence": "Sequenza", + "reset": "Reset", + "status": "Stato", + "monthly": "Mensile", + "yearly": "Annuale", + "never": "Mai", + "previewTooltip": "Anteprima prossimo codice", + "resetTooltip": "Reset sequenza", + "resetConfirmTitle": "Conferma Reset Sequenza", + "resetConfirmText": "Sei sicuro di voler resettare la sequenza per", + "resetWarning": "La sequenza verrà riportata a 0. Il prossimo codice generato partirà da 1.", + "previewTitle": "Anteprima Prossimo Codice", + "previewText": "Questo è il codice che verrà generato alla prossima creazione.\nLa sequenza non è stata incrementata.", + "helpTitle": "Guida ai Pattern", + "helpText": "I pattern definiscono come vengono generati i codici automatici. Puoi combinare testo statico e placeholder dinamici.", + "placeholders": "Placeholder Disponibili", + "examples": "Esempi di Pattern", + "editTitle": "Modifica Configurazione", + "prefixHelper": "Testo sostituito nel placeholder {PREFIX}", + "patternHelper": "Pattern per generazione codice", + "previewLabel": "Anteprima:", + "resetSequence": "Reset Sequenza", + "everyYear": "Ogni anno", + "everyMonth": "Ogni mese", + "generationActive": "Generazione attiva", + "readOnly": "Codice non modificabile" + }, + "customFields": { + "title": "Gestione Campi Personalizzati", + "sectionTitle": "Campi Personalizzati", + "entity": "Entità", + "newField": "Nuovo Campo", + "label": "Etichetta", + "fieldName": "Nome Interno", + "type": "Tipo", + "required": "Obbligatorio", + "order": "Ordine", + "editField": "Modifica Campo", + "deleteConfirm": "Sei sicuro di voler eliminare questo campo?", + "fieldNameHelper": "Deve essere univoco per l'entità. Usa solo lettere minuscole e underscore.", + "optionsJson": "Opzioni (JSON Array)", + "optionsHelper": "Inserisci un array JSON valido di stringhe", + "description": "Descrizione / Helper Text", + "noFields": "Nessun campo personalizzato configurato per questa entità.", + "types": { + "text": "Testo", + "number": "Numero", + "date": "Data", + "boolean": "Booleano (Sì/No)", + "select": "Lista a discesa", + "multiselect": "Selezione Multipla", + "textarea": "Area di testo", + "color": "Colore", + "url": "URL", + "email": "Email" + }, + "entities": { + "client": "Clienti", + "article": "Articoli (Catering)", + "event": "Eventi", + "warehousearticle": "Articoli Magazzino", + "warehouselocation": "Magazzini", + "resource": "Risorse (Staff)" + } + }, + "reports": { + "title": "Template Report", + "import": "Importa", + "newTemplate": "Nuovo Template", + "filterCategory": "Filtra per categoria", + "all": "Tutte", + "importTemplate": "Importa Template", + "noTemplates": "Nessun template trovato", + "createFirstTemplate": "Crea il tuo primo template di report o importane uno esistente", + "createTemplate": "Crea Template", + "vertical": "Verticale", + "horizontal": "Orizzontale", + "edit": "Modifica", + "duplicate": "Duplica", + "export": "Esporta", + "delete": "Elimina", + "confirmDelete": "Conferma Eliminazione", + "deleteConfirmText": "Sei sicuro di voler eliminare il template \"{{name}}\"?", + "irreversibleAction": "Questa azione non può essere annullata.", + "cancel": "Annulla", + "deleting": "Eliminazione...", + "importTitle": "Importa Template", + "importText": "Seleziona un file .aprt da importare", + "selectFile": "Seleziona File", + "importing": "Importazione...", + "categories": { + "Evento": "Evento", + "Cliente": "Cliente", + "Articoli": "Articoli", + "Generale": "Generale", + "Importato": "Importato" + }, + "editor": { + "newTemplate": "Nuovo Template", + "templateUpdatedByOther": "Template aggiornato da un altro utente", + "deleteSuccess": "Evento eliminato con successo", + "detail": { + "status": { + "draft": "Scheda Evento", + "quote": "Preventivo", + "confirmed": "Confermato", + "new": "Nuovo" + }, + "loading": "Caricamento...", + "newEvent": "Nuovo Evento", + "noDescription": "Senza descrizione", + "actions": { + "duplicate": "Duplica", + "recalculate": "Ricalcola Qta", + "confirm": "Conferma", + "save": "Salva", + "print": "Stampa PDF", + "back": "Indietro" + }, + "fields": { + "date": "Data Evento", + "startTime": "Ora Inizio", + "endTime": "Ora Fine", + "type": "Tipo Evento", + "description": "Descrizione Evento", + "descriptionPlaceholder": "es. Matrimonio Rossi-Bianchi", + "client": "Cliente", + "location": "Location", + "totalGuests": "N. Ospiti Totale", + "costPerPerson": "Costo a Persona", + "totalCost": "Costo Totale", + "totalDeposits": "Totale Acconti", + "balance": "Saldo", + "status": "Stato" + }, + "tabs": { + "guests": "Ospiti", + "withdrawalList": "Lista Prelievo", + "resources": "Risorse", + "costs": "Costi", + "notes": "Note" + }, + "guestsTab": { + "total": "Totale ospiti", + "add": "Aggiungi Tipo Ospite", + "type": "Tipo Ospite", + "quantity": "Quantità", + "notes": "Note", + "empty": "Nessun ospite aggiunto. Clicca \"Aggiungi Tipo Ospite\" per iniziare." + }, + "withdrawalTab": { + "total": "Articoli in lista", + "add": "Aggiungi Articolo", + "code": "Codice", + "article": "Articolo", + "qtyRequested": "Qta Richiesta", + "qtyCalculated": "Qta Calcolata", + "qtyActual": "Qta Effettiva", + "notes": "Note", + "empty": "Nessun articolo in lista." + }, + "resourcesTab": { + "total": "Risorse impegnate", + "add": "Aggiungi Risorsa", + "resource": "Risorsa", + "quantity": "Quantità", + "costUnit": "Costo Unitario", + "costTotal": "Costo Totale", + "notes": "Note", + "empty": "Nessuna risorsa aggiunta." + }, + "dialogs": { + "addGuest": "Aggiungi Ospite", + "addArticle": "Aggiungi Articolo", + "addResource": "Aggiungi Risorsa", + "cancel": "Annulla", + "add": "Aggiungi" + } + }, + "saveSuccess": "Template salvato con successo", + "saveError": "Errore nel salvataggio: {{error}}", + "pageName": "Pagina {{number}}", + "copyOf": "{{name}} (copia)", + "newText": "Nuovo testo", + "column": "Colonna {{number}}", + "elementCopied": "Elemento copiato", + "pastedSuffix": "_incollato", + "copySuffix": "_copia", + "groupingNotImplemented": "Raggruppamento non ancora implementato", + "ungroupingNotImplemented": "Separazione non ancora implementata", + "doubleClickToEdit": "Fai doppio click sul testo per modificarlo", + "fitToContentNotImplemented": "Adatta al contenuto non ancora implementato", + "selectDatasetForPreview": "Seleziona almeno un dataset per l'anteprima", + "saveBeforePreview": "Salva il template prima di visualizzare l'anteprima", + "previewError": "Errore nella generazione dell'anteprima: {{error}}", + "panels": { + "pages": "Pagine", + "data": "Campi Dati", + "properties": "Proprietà" + }, + "defaultPageName": "Pagina 1", + "saveDialog": { + "title": "Salva Template", + "name": "Nome", + "description": "Descrizione", + "category": "Categoria", + "cancel": "Annulla", + "saving": "Salvataggio...", + "save": "Salva" + } + } + }, + "warehouse": { + "dashboard": { + "newInbound": "Nuovo Carico", + "stockLevels": "Giacenze", + "activeArticles": "Articoli Attivi", + "warehouses": "Magazzini", + "totalValue": "Valore Totale", + "lowStock": "Sotto Scorta", + "outOfStock": "esauriti", + "recentMovements": "Ultimi Movimenti", + "viewAll": "Vedi tutti", + "noRecentMovements": "Nessun movimento recente", + "lines": "righe", + "manage": "Gestisci", + "draftMovements": "movimenti in bozza da confermare", + "view": "Visualizza", + "expiringBatches": "lotti in scadenza nei prossimi 30 giorni", + "lowStockArticles": "Articoli Sotto Scorta", + "noLowStockArticles": "Nessun articolo sotto scorta", + "quickActions": "Azioni Rapide", + "inbound": "Carico", + "outbound": "Scarico", + "transfer": "Trasferimento", + "newArticle": "Nuovo Articolo", + "inventory": "Inventario", + "valuation": "Valorizzazione" + }, + "articles": { + "title": "Anagrafica Articoli", + "newArticle": "Nuovo Articolo", + "columns": { + "code": "Codice", + "description": "Descrizione", + "category": "Categoria", + "uom": "U.M.", + "averageCost": "Costo Medio", + "status": "Stato", + "active": "Attivo", + "inactive": "Inattivo" + }, + "filters": { + "searchPlaceholder": "Cerca per codice o descrizione...", + "category": "Categoria", + "all": "Tutte", + "showAll": "Mostra Tutti", + "onlyActive": "Solo Attivi", + "viewList": "Vista Lista", + "viewGrid": "Vista Griglia" + }, + "loadingError": "Errore nel caricamento degli articoli: {{error}}", + "noArticlesFound": "Nessun articolo trovato", + "actions": { + "edit": "Modifica", + "viewStock": "Visualizza Giacenze", + "delete": "Elimina" + }, + "deleteDialog": { + "title": "Conferma Eliminazione", + "content": "Sei sicuro di voler eliminare l'articolo {{code}} - {{description}}?", + "warning": "Questa azione non può essere annullata.", + "cancel": "Annulla", + "deleting": "Eliminazione...", + "delete": "Elimina" + } + }, + "articleForm": { + "titleNew": "Nuovo Articolo", + "titleEdit": "Articolo: {{code}}", + "tabs": { + "general": "Dati Generali", + "stock": "Giacenze", + "batches": "Lotti", + "serials": "Matricole" + }, + "sections": { + "basicInfo": "Informazioni Base", + "stockLevels": "Livelli di Scorta", + "costs": "Costi e Valorizzazione", + "traceability": "Tracciabilità", + "image": "Immagine", + "summary": "Riepilogo" + }, + "fields": { + "code": "Codice", + "alternativeCode": "Codice Alternativo", + "description": "Descrizione", + "shortDescription": "Descrizione Breve", + "category": "Categoria", + "uom": "Unità di Misura", + "barcode": "Codice a Barre", + "notes": "Note", + "minStock": "Scorta Minima", + "maxStock": "Scorta Massima", + "reorderPoint": "Punto di Riordino", + "reorderQuantity": "Quantità Riordino", + "standardCost": "Costo Standard", + "stockManagement": "Gestione Stock", + "valuationMethod": "Metodo di Valorizzazione", + "batchManaged": "Gestione Lotti", + "serialManaged": "Gestione Matricole", + "expiryManaged": "Gestione Scadenza", + "active": "Articolo Attivo" + }, + "helpers": { + "generatedOnSave": "(Generato al salvataggio)", + "willBeGenerated": "Verrà assegnato automaticamente", + "generatedAutomatically": "Generato automaticamente", + "optional": "Opzionale" + }, + "validation": { + "codeRequired": "Il codice è obbligatorio", + "descriptionRequired": "La descrizione è obbligatoria", + "uomRequired": "L'unità di misura è obbligatoria" + }, + "errors": { + "saveError": "Errore durante il salvataggio: {{error}}" + }, + "actions": { + "upload": "Carica", + "cancel": "Annulla", + "save": "Salva", + "saving": "Salvataggio..." + }, + "summary": { + "averageCost": "Costo Medio", + "lastPurchase": "Ultimo Acquisto" + }, + "tables": { + "warehouse": "Magazzino", + "quantity": "Quantità", + "reserved": "Riservata", + "available": "Disponibile", + "value": "Valore", + "batchNumber": "Numero Lotto", + "expiryDate": "Data Scadenza", + "status": "Stato", + "serialNumber": "Matricola", + "lot": "Lotto", + "noStock": "Nessuna giacenza", + "noBatches": "Nessun lotto", + "noSerials": "Nessuna matricola" + }, + "status": { + "expired": "Scaduto", + "available": "Disponibile", + "unavailable": "Non disponibile" + }, + "options": { + "noCategory": "Nessuna" + } + }, + "stockManagementType": { + "Standard": "Standard", + "NotManaged": "Non Gestito", + "VariableWeight": "Peso Variabile", + "Kit": "Kit" + }, + "valuationMethod": { + "WeightedAverage": "Costo Medio Ponderato", + "FIFO": "FIFO", + "LIFO": "LIFO", + "StandardCost": "Costo Standard", + "SpecificCost": "Costo Specifico" + }, + "movements": { + "title": "Movimenti di Magazzino", + "filters": { + "searchPlaceholder": "Cerca documento, riferimento...", + "warehouse": "Magazzino", + "all": "Tutti", + "type": "Tipo", + "status": "Stato", + "from": "Da", + "to": "A", + "reset": "Reset" + }, + "columns": { + "document": "Documento", + "date": "Data", + "type": "Tipo", + "status": "Stato", + "warehouse": "Magazzino", + "destination": "Destinazione", + "reason": "Causale", + "lines": "Righe", + "value": "Valore", + "reference": "Riferimento" + }, + "actions": { + "newMovement": "Nuovo Movimento", + "inbound": "Carico", + "outbound": "Scarico", + "transfer": "Trasferimento", + "adjustment": "Rettifica", + "view": "Visualizza", + "confirm": "Conferma", + "cancel": "Annulla", + "delete": "Elimina" + }, + "dialogs": { + "confirm": { + "title": "Conferma Movimento", + "content": "Confermare il movimento {{doc}}?", + "warning": "Le giacenze verranno aggiornate e il movimento non potrà più essere modificato.", + "confirming": "Conferma...", + "confirm": "Conferma", + "cancel": "Annulla" + }, + "cancel": { + "title": "Annulla Movimento", + "content": "Annullare il movimento {{doc}}?", + "warning": "Il movimento verrà marcato come annullato ma non eliminato.", + "cancelling": "Annullamento...", + "cancelMovement": "Annulla Movimento", + "back": "Indietro" + }, + "delete": { + "title": "Elimina Movimento", + "content": "Eliminare definitivamente il movimento {{doc}}?", + "warning": "Questa azione non può essere annullata.", + "deleting": "Eliminazione...", + "delete": "Elimina", + "cancel": "Annulla" + } + }, + "loadingError": "Errore nel caricamento dei movimenti: {{error}}" + }, + "movementType": { + "Inbound": "Carico", + "Outbound": "Scarico", + "Transfer": "Trasferimento", + "Adjustment": "Rettifica", + "Production": "Produzione", + "Consumption": "Consumo", + "SupplierReturn": "Reso Fornitore", + "CustomerReturn": "Reso Cliente" + }, + "movementStatus": { + "Draft": "Bozza", + "Confirmed": "Confermato", + "Cancelled": "Annullato" + }, + "inbound": { + "title": "Nuovo Carico", + "subtitle": "Movimento di entrata merce in magazzino", + "sections": { + "movementData": "Dati Movimento", + "lines": "Righe Movimento" + }, + "fields": { + "date": "Data Movimento", + "warehouse": "Magazzino", + "documentNumber": "Numero Documento", + "externalReference": "Riferimento Esterno", + "notes": "Note", + "article": "Articolo", + "quantity": "Quantità", + "unitCost": "Costo Unitario", + "total": "Totale" + }, + "placeholders": { + "documentNumber": "DDT, Fattura, etc.", + "externalReference": "Ordine, Fornitore, etc.", + "selectArticle": "Seleziona articolo" + }, + "actions": { + "addLine": "Aggiungi Riga", + "cancel": "Annulla", + "saveDraft": "Salva Bozza", + "saveAndConfirm": "Salva e Conferma" + }, + "totals": { + "quantity": "Totale Quantità", + "value": "Totale Valore" + }, + "validation": { + "warehouseRequired": "Seleziona un magazzino", + "dateRequired": "Inserisci la data", + "linesRequired": "Inserisci almeno una riga con articolo e quantità" + }, + "errors": { + "saveError": "Errore: {{error}}" + } + }, + "outbound": { + "title": "Nuovo Scarico", + "subtitle": "Movimento di uscita merce da magazzino", + "warnings": { + "stockIssues": "Attenzione: alcune righe superano la disponibilità in magazzino", + "overStock": "Quantità superiore alla disponibilità" + }, + "sections": { + "movementData": "Dati Movimento", + "lines": "Righe Movimento" + }, + "fields": { + "date": "Data Movimento", + "warehouse": "Magazzino", + "documentNumber": "Numero Documento", + "externalReference": "Riferimento Esterno", + "notes": "Note", + "article": "Articolo", + "available": "Disponibile", + "quantity": "Quantità" + }, + "placeholders": { + "documentNumber": "DDT, Bolla, etc.", + "externalReference": "Ordine, Cliente, etc.", + "selectArticle": "Seleziona articolo", + "notes": "Note" + }, + "actions": { + "addLine": "Aggiungi Riga", + "cancel": "Annulla", + "saveDraft": "Salva Bozza", + "saveAndConfirm": "Salva e Conferma" + }, + "totals": { + "quantity": "Totale Quantità" + }, + "validation": { + "warehouseRequired": "Seleziona un magazzino", + "dateRequired": "Inserisci la data", + "linesRequired": "Inserisci almeno una riga con articolo e quantità" + }, + "errors": { + "saveError": "Errore: {{error}}" + } + }, + "transfer": { + "title": "Trasferimento tra Magazzini", + "subtitle": "Sposta merce da un magazzino all'altro", + "sections": { + "transferData": "Dati Trasferimento", + "lines": "Articoli da Trasferire" + }, + "fields": { + "date": "Data", + "sourceWarehouse": "Magazzino Origine", + "destWarehouse": "Magazzino Destinazione", + "document": "Documento", + "externalReference": "Riferimento Esterno", + "notes": "Note", + "article": "Articolo", + "available": "Disponibile", + "quantity": "Quantità" + }, + "placeholders": { + "article": "Articolo", + "notes": "Note" + }, + "actions": { + "add": "Aggiungi", + "cancel": "Annulla", + "saveDraft": "Salva Bozza", + "saveAndConfirm": "Salva e Conferma" + }, + "totals": { + "total": "Totale: {{value}}" + }, + "validation": { + "sourceRequired": "Seleziona magazzino origine", + "destRequired": "Seleziona magazzino destinazione", + "sameWarehouse": "Origine e destinazione devono essere diversi", + "dateRequired": "Inserisci la data", + "linesRequired": "Inserisci almeno una riga" + }, + "errors": { + "saveError": "Errore: {{error}}" + } + }, + "locations": { + "title": "Gestione Magazzini", + "newWarehouse": "Nuovo Magazzino", + "emptyState": { + "title": "Nessun magazzino configurato", + "action": "Aggiungi il primo magazzino" + }, + "card": { + "default": "Magazzino Predefinito", + "inactive": "Inattivo", + "setDefault": "Imposta come predefinito", + "edit": "Modifica", + "delete": "Elimina" + }, + "dialog": { + "createTitle": "Nuovo Magazzino", + "editTitle": "Modifica Magazzino", + "fields": { + "code": "Codice", + "alternativeCode": "Codice Alternativo", + "name": "Nome", + "description": "Descrizione", + "type": "Tipo", + "address": "Indirizzo", + "isDefault": "Magazzino Predefinito", + "isActive": "Attivo" + }, + "helpers": { + "generatedOnSave": "(Generato al salvataggio)", + "generatedAutomatically": "Generato automaticamente", + "willBeAssigned": "Verrà assegnato automaticamente", + "optional": "Opzionale" + }, + "validation": { + "nameRequired": "Il nome è obbligatorio" + }, + "actions": { + "cancel": "Annulla", + "save": "Salva", + "saving": "Salvataggio..." + } + }, + "deleteDialog": { + "title": "Conferma Eliminazione", + "content": "Sei sicuro di voler eliminare il magazzino {{code}} - {{name}}?", + "warning": "Questa azione non può essere annullata.", + "deleting": "Eliminazione...", + "delete": "Elimina", + "cancel": "Annulla" + }, + "loadingError": "Errore nel caricamento dei magazzini: {{error}}" + }, + "warehouseType": { + "Physical": "Fisico", + "Transit": "Transito", + "Returns": "Resi", + "Defective": "Difettosi", + "Subcontract": "Conto Lavoro" + }, + "stockLevels": { + "title": "Giacenze di Magazzino", + "valuation": "Valorizzazione", + "summary": { + "articles": "Articoli", + "totalQuantity": "Quantità Totale", + "totalValue": "Valore Totale", + "lowStock": "Sotto Scorta" + }, + "filters": { + "search": "Cerca articolo...", + "warehouse": "Magazzino", + "category": "Categoria", + "lowStockOnly": "Solo sotto scorta", + "allWarehouses": "Tutti", + "allCategories": "Tutte" + }, + "columns": { + "code": "Codice", + "article": "Articolo", + "warehouse": "Magazzino", + "category": "Categoria", + "quantity": "Giacenza", + "reserved": "Riservata", + "available": "Disponibile", + "averageCost": "Costo Medio", + "value": "Valore" + }, + "error": "Errore: {{error}}" + }, + "inventory": { + "title": "Inventari Fisici", + "newInventory": "Nuovo Inventario", + "status": { + "Draft": "Bozza", + "InProgress": "In Corso", + "Completed": "Completato", + "Confirmed": "Confermato", + "Cancelled": "Annullato" + }, + "columns": { + "code": "Codice", + "description": "Descrizione", + "date": "Data Inventario", + "warehouse": "Magazzino", + "category": "Categoria", + "status": "Stato", + "progress": "Progresso", + "actions": "Azioni" + }, + "actions": { + "view": "Dettaglio", + "start": "Avvia Conteggio", + "continue": "Continua Conteggio", + "cancel": "Annulla" + }, + "confirmCancel": "Sei sicuro di voler annullare questo inventario?", + "form": { + "title": { + "new": "Nuovo Inventario", + "edit": "Modifica Inventario", + "editWithCode": "Inventario {{code}}" + }, + "breadcrumbs": { + "list": "Inventari" + }, + "fields": { + "description": "Descrizione", + "date": "Data Inventario", + "warehouse": "Magazzino", + "category": "Categoria (Opzionale)", + "type": "Tipo Inventario", + "notes": "Note" + }, + "options": { + "allCategories": "Tutte", + "type": { + "Full": "Completo", + "Partial": "Parziale", + "Cyclic": "Ciclico", + "Sample": "A Campione" + } + }, + "actions": { + "back": "Indietro", + "save": "Salva Modifiche", + "create": "Crea e Inizia" + } + }, + "count": { + "title": "Inventario: {{description}}", + "actions": { + "back": "Indietro", + "start": "Avvia Inventario", + "complete": "Completa Conteggio", + "confirm": "Conferma e Rettifica" + }, + "cards": { + "date": "Data Inventario", + "warehouse": "Magazzino", + "totalLines": "Righe Totali", + "countedLines": "Righe Contate" + }, + "alert": { + "completed": "L'inventario è completato. Verifica le differenze prima di confermare. La conferma genererà automaticamente i movimenti di rettifica." + }, + "columns": { + "articleCode": "Codice Articolo", + "description": "Descrizione", + "batch": "Lotto", + "location": "Ubicazione", + "theoreticalQty": "Qta Teorica", + "countedQty": "Qta Contata", + "difference": "Differenza" + }, + "confirmDialog": { + "title": "Conferma Inventario", + "content": "Sei sicuro di voler confermare l'inventario? Questa operazione è irreversibile e genererà i movimenti di rettifica per le differenze riscontrate.", + "cancel": "Annulla", + "confirm": "Conferma" + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index ec0e331..df61b16 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -33,36 +33,19 @@ import { } from "@mui/icons-material"; import CollaborationIndicator from "./collaboration/CollaborationIndicator"; import { useModules } from "../contexts/ModuleContext"; +import { useLanguage } from "../contexts/LanguageContext"; import { SettingsSelector } from "./SettingsSelector"; const DRAWER_WIDTH = 240; const DRAWER_WIDTH_COLLAPSED = 64; -const menuItems = [ - { text: "Dashboard", icon: , path: "/" }, - { text: "Calendario", icon: , path: "/calendario" }, - { text: "Eventi", icon: , path: "/eventi" }, - { text: "Clienti", icon: , path: "/clienti" }, - { text: "Location", icon: , path: "/location" }, - { text: "Articoli", icon: , path: "/articoli" }, - { text: "Risorse", icon: , path: "/risorse" }, - { - text: "Magazzino", - icon: , - path: "/warehouse", - moduleCode: "warehouse", - }, - { text: "Report", icon: , path: "/report-templates" }, - { text: "Moduli", icon: , path: "/modules" }, - { text: "Codici Auto", icon: , path: "/admin/auto-codes" }, -]; - export default function Layout() { const [mobileOpen, setMobileOpen] = useState(false); const navigate = useNavigate(); const location = useLocation(); const theme = useTheme(); const { activeModules } = useModules(); + const { t } = useLanguage(); // Breakpoints const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px @@ -71,6 +54,26 @@ export default function Layout() { // Drawer width based on screen size const drawerWidth = isTablet ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH; + const menuItems = [ + { text: t('menu.dashboard'), icon: , path: "/" }, + { text: t('menu.calendar'), icon: , path: "/calendario" }, + { text: t('menu.events'), icon: , path: "/eventi" }, + { text: t('menu.clients'), icon: , path: "/clienti" }, + { text: t('menu.location'), icon: , path: "/location" }, + { text: t('menu.articles'), icon: , path: "/articoli" }, + { text: t('menu.resources'), icon: , path: "/risorse" }, + { + text: t('menu.warehouse'), + icon: , + path: "/warehouse", + moduleCode: "warehouse", + }, + { text: t('menu.reports'), icon: , path: "/report-templates" }, + { text: t('menu.modules'), icon: , path: "/modules" }, + { text: t('menu.autoCodes'), icon: , path: "/admin/auto-codes" }, + { text: t('menu.customFields'), icon: , path: "/admin/custom-fields" }, + ]; + // Filter menu items based on active modules const activeModuleCodes = activeModules.map((m) => m.code); const filteredMenuItems = menuItems.filter( diff --git a/frontend/src/components/SettingsSelector.tsx b/frontend/src/components/SettingsSelector.tsx index 98bebbc..e160a5f 100644 --- a/frontend/src/components/SettingsSelector.tsx +++ b/frontend/src/components/SettingsSelector.tsx @@ -46,7 +46,7 @@ export const SettingsSelector: React.FC = () => { return ( <> - + { {mode === 'dark' ? : } - {mode === 'dark' ? t('light') : t('dark')} + {mode === 'dark' ? t('common.light') : t('common.dark')} diff --git a/frontend/src/components/customFields/CustomFieldsRenderer.tsx b/frontend/src/components/customFields/CustomFieldsRenderer.tsx index e58891c..b127c2c 100644 --- a/frontend/src/components/customFields/CustomFieldsRenderer.tsx +++ b/frontend/src/components/customFields/CustomFieldsRenderer.tsx @@ -9,6 +9,7 @@ import { } from '@mui/material'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { CustomFieldDefinition, CustomFieldType, CustomFieldValues } from '../../types/customFields'; +import { useTranslation } from 'react-i18next'; import { customFieldService } from '../../services/customFieldService'; import dayjs from 'dayjs'; @@ -20,6 +21,7 @@ interface Props { } export const CustomFieldsRenderer: React.FC = ({ entityName, values, onChange, readOnly = false }) => { + const { t } = useTranslation(); const [definitions, setDefinitions] = useState([]); const [loading, setLoading] = useState(true); @@ -43,7 +45,7 @@ export const CustomFieldsRenderer: React.FC = ({ entityName, values, onCh return ( - Campi Personalizzati + {t("customFields.sectionTitle")} {definitions.map(def => ( diff --git a/frontend/src/contexts/LanguageContext.tsx b/frontend/src/contexts/LanguageContext.tsx index 385527f..50ef0f5 100644 --- a/frontend/src/contexts/LanguageContext.tsx +++ b/frontend/src/contexts/LanguageContext.tsx @@ -1,4 +1,5 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; +import React, { createContext, useContext, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import 'dayjs/locale/it'; import 'dayjs/locale/en'; @@ -21,63 +22,19 @@ const LanguageContext = createContext({ export const useLanguage = () => useContext(LanguageContext); -// Simple translation dictionary for demo purposes -const translations: Record> = { - it: { - 'settings': 'Impostazioni', - 'theme': 'Tema', - 'language': 'Lingua', - 'dark': 'Scuro', - 'light': 'Chiaro', - 'logout': 'Esci', - 'dashboard': 'Dashboard', - 'calendar': 'Calendario', - 'events': 'Eventi', - 'clients': 'Clienti', - 'location': 'Location', - 'articles': 'Articoli', - 'resources': 'Risorse', - 'warehouse': 'Magazzino', - 'reports': 'Report', - 'modules': 'Moduli', - 'autoCodes': 'Codici Auto', - }, - en: { - 'settings': 'Settings', - 'theme': 'Theme', - 'language': 'Language', - 'dark': 'Dark', - 'light': 'Light', - 'logout': 'Logout', - 'dashboard': 'Dashboard', - 'calendar': 'Calendar', - 'events': 'Events', - 'clients': 'Clients', - 'location': 'Location', - 'articles': 'Articles', - 'resources': 'Resources', - 'warehouse': 'Warehouse', - 'reports': 'Reports', - 'modules': 'Modules', - 'autoCodes': 'Auto Codes', - } -}; - export const AppLanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [language, setLanguage] = useState(() => { - const savedLang = localStorage.getItem('language'); - return (savedLang as Language) || 'it'; - }); + const { t, i18n } = useTranslation(); + + const language = (i18n.language?.split('-')[0] as Language) || 'it'; + + const setLanguage = (lang: Language) => { + i18n.changeLanguage(lang); + }; useEffect(() => { - localStorage.setItem('language', language); dayjs.locale(language); }, [language]); - const t = (key: string) => { - return translations[language][key] || key; - }; - return ( diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..86aa4f4 --- /dev/null +++ b/frontend/src/i18n.ts @@ -0,0 +1,35 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) + // learn more: https://github.com/i18next/i18next-http-backend + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languagedetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: 'it', + debug: import.meta.env.DEV, + + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + + backend: { + loadPath: '/locales/{{lng}}/{{ns}}.json', + }, + + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + }, + }); + +export default i18n; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..d9ed4c8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,13 @@ -import { StrictMode } from 'react' +import { StrictMode, Suspense } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './i18n' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - + Loading...}> + + , ) diff --git a/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx index 78050cd..1e8c183 100644 --- a/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx +++ b/frontend/src/modules/warehouse/pages/ArticleFormPage.tsx @@ -37,6 +37,7 @@ import { Delete as DeleteIcon, Image as ImageIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { useArticle, useCreateArticle, @@ -78,6 +79,7 @@ function TabPanel(props: TabPanelProps) { } export default function ArticleFormPage() { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const isNew = !id || id === "new"; @@ -194,13 +196,13 @@ export default function ArticleFormPage() { const newErrors: Record = {}; // Il codice è generato automaticamente, non richiede validazione in creazione if (!isNew && !formData.code.trim()) { - newErrors.code = "Il codice è obbligatorio"; + newErrors.code = t("warehouse.articleForm.validation.codeRequired"); } if (!formData.description.trim()) { - newErrors.description = "La descrizione è obbligatoria"; + newErrors.description = t("warehouse.articleForm.validation.descriptionRequired"); } if (!formData.unitOfMeasure.trim()) { - newErrors.unitOfMeasure = "L'unità di misura è obbligatoria"; + newErrors.unitOfMeasure = t("warehouse.articleForm.validation.uomRequired"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -300,24 +302,23 @@ export default function ArticleFormPage() { - {isNew ? "Nuovo Articolo" : `Articolo: ${article?.code}`} + {isNew ? t("warehouse.articleForm.titleNew") : t("warehouse.articleForm.titleEdit", { code: article?.code })} {(createMutation.error || updateMutation.error) && ( - Errore durante il salvataggio:{" "} - {((createMutation.error || updateMutation.error) as Error).message} + {t("warehouse.articleForm.errors.saveError", { error: ((createMutation.error || updateMutation.error) as Error).message })} )} {!isNew && ( setTabValue(v)}> - - - {article?.isBatchManaged && } - {article?.isSerialManaged && } + + + {article?.isBatchManaged && } + {article?.isSerialManaged && } )} @@ -329,21 +330,21 @@ export default function ArticleFormPage() { - Informazioni Base + {t("warehouse.articleForm.sections.basicInfo")} @@ -363,18 +364,18 @@ export default function ArticleFormPage() { handleChange("alternativeCode", e.target.value) } - helperText="Opzionale" + helperText={t("warehouse.articleForm.helpers.optional")} /> handleChange("description", e.target.value) @@ -387,7 +388,7 @@ export default function ArticleFormPage() { handleChange("shortDescription", e.target.value) @@ -396,10 +397,10 @@ export default function ArticleFormPage() { - Categoria + {t("warehouse.articleForm.fields.category")} handleChange("stockManagement", e.target.value) } > {Object.entries(stockManagementTypeLabels).map( - ([value, label]) => ( + ([value]) => ( - {label} + {t(`warehouse.stockManagementType.${StockManagementType[parseInt(value, 10)]}`)} ), )} @@ -557,18 +558,18 @@ export default function ArticleFormPage() { - Metodo di Valorizzazione + {t("warehouse.articleForm.fields.valuationMethod")} - Riepilogo + {t("warehouse.articleForm.sections.summary")} - Costo Medio: + {t("warehouse.articleForm.summary.averageCost")}: {formatCurrency(article.weightedAverageCost || 0)} @@ -732,7 +733,7 @@ export default function ArticleFormPage() { sx={{ display: "flex", justifyContent: "space-between" }} > - Ultimo Acquisto: + {t("warehouse.articleForm.summary.lastPurchase")}: {formatCurrency(article.lastPurchaseCost || 0)} @@ -746,7 +747,7 @@ export default function ArticleFormPage() { {/* Submit Button */} - + @@ -767,17 +768,17 @@ export default function ArticleFormPage() { - Giacenze per Magazzino + {t("warehouse.articleForm.sections.stockLevels")} - Magazzino - Quantità - Riservata - Disponibile - Valore + {t("warehouse.articleForm.tables.warehouse")} + {t("warehouse.articleForm.tables.quantity")} + {t("warehouse.articleForm.tables.reserved")} + {t("warehouse.articleForm.tables.available")} + {t("warehouse.articleForm.tables.value")} @@ -785,7 +786,7 @@ export default function ArticleFormPage() { - Nessuna giacenza + {t("warehouse.articleForm.tables.noStock")} @@ -822,16 +823,16 @@ export default function ArticleFormPage() { - Lotti + {t("warehouse.articleForm.tabs.batches")}
- Numero Lotto - Quantità - Data Scadenza - Stato + {t("warehouse.articleForm.tables.batchNumber")} + {t("warehouse.articleForm.tables.quantity")} + {t("warehouse.articleForm.tables.expiryDate")} + {t("warehouse.articleForm.tables.status")} @@ -839,7 +840,7 @@ export default function ArticleFormPage() { - Nessun lotto + {t("warehouse.articleForm.tables.noBatches")} @@ -858,7 +859,7 @@ export default function ArticleFormPage() { @@ -878,16 +879,16 @@ export default function ArticleFormPage() { - Matricole + {t("warehouse.articleForm.tabs.serials")}
- Matricola - Magazzino - Lotto - Stato + {t("warehouse.articleForm.tables.serialNumber")} + {t("warehouse.articleForm.tables.warehouse")} + {t("warehouse.articleForm.tables.lot")} + {t("warehouse.articleForm.tables.status")} @@ -895,7 +896,7 @@ export default function ArticleFormPage() { - Nessuna matricola + {t("warehouse.articleForm.tables.noSerials")} @@ -911,8 +912,8 @@ export default function ArticleFormPage() { ("list"); const [search, setSearch] = useState(""); const [categoryId, setCategoryId] = useState(""); @@ -167,7 +169,7 @@ export default function ArticlesPage() { }, { field: "code", - headerName: "Codice", + headerName: t("warehouse.articles.columns.code"), width: 120, renderCell: (params: GridRenderCellParams) => ( @@ -177,24 +179,24 @@ export default function ArticlesPage() { }, { field: "description", - headerName: "Descrizione", + headerName: t("warehouse.articles.columns.description"), flex: 1, minWidth: 200, }, { field: "categoryName", - headerName: "Categoria", + headerName: t("warehouse.articles.columns.category"), width: 150, }, { field: "unitOfMeasure", - headerName: "U.M.", + headerName: t("warehouse.articles.columns.uom"), width: 80, align: "center", }, { field: "weightedAverageCost", - headerName: "Costo Medio", + headerName: t("warehouse.articles.columns.averageCost"), width: 120, align: "right", renderCell: (params: GridRenderCellParams) => @@ -202,11 +204,11 @@ export default function ArticlesPage() { }, { field: "isActive", - headerName: "Stato", + headerName: t("warehouse.articles.columns.status"), width: 100, renderCell: (params: GridRenderCellParams) => ( @@ -229,7 +231,7 @@ export default function ArticlesPage() { return ( - Errore nel caricamento degli articoli: {(error as Error).message} + {t("warehouse.articles.loadingError", { error: (error as Error).message })} ); @@ -247,14 +249,14 @@ export default function ArticlesPage() { }} > - Anagrafica Articoli + {t("warehouse.articles.title")} @@ -265,7 +267,7 @@ export default function ArticlesPage() { setSearch(e.target.value)} InputProps={{ @@ -286,14 +288,14 @@ export default function ArticlesPage() { - Categoria + {t("warehouse.articles.filters.category")} setWarehouseId(e.target.value as number)} > {warehouses?.map((w) => ( @@ -242,25 +243,25 @@ export default function InboundMovementPage() { setDocumentNumber(e.target.value)} - placeholder="DDT, Fattura, etc." + placeholder={t("warehouse.inbound.placeholders.documentNumber")} /> setExternalReference(e.target.value)} - placeholder="Ordine, Fornitore, etc." + placeholder={t("warehouse.inbound.placeholders.externalReference")} /> setNotes(e.target.value)} multiline @@ -280,9 +281,9 @@ export default function InboundMovementPage() { mb: 2, }} > - Righe Movimento + {t("warehouse.inbound.sections.lines")} @@ -296,15 +297,15 @@ export default function InboundMovementPage() {
- Articolo + {t("warehouse.inbound.fields.article")} - Quantità + {t("warehouse.inbound.fields.quantity")} - Costo Unitario + {t("warehouse.inbound.fields.unitCost")} - Totale + {t("warehouse.inbound.fields.total")} @@ -326,7 +327,7 @@ export default function InboundMovementPage() { )} isOptionEqualToValue={(option, value) => @@ -410,13 +411,13 @@ export default function InboundMovementPage() { - Totale Quantità + {t("warehouse.inbound.totals.quantity")} {totalQuantity.toFixed(2)} - Totale Valore + {t("warehouse.inbound.totals.value")} {formatCurrency(totalValue)} @@ -425,7 +426,7 @@ export default function InboundMovementPage() { {/* Actions */} - + diff --git a/frontend/src/modules/warehouse/pages/InventoryCountPage.tsx b/frontend/src/modules/warehouse/pages/InventoryCountPage.tsx index adf4ea5..3ca4806 100644 --- a/frontend/src/modules/warehouse/pages/InventoryCountPage.tsx +++ b/frontend/src/modules/warehouse/pages/InventoryCountPage.tsx @@ -31,11 +31,13 @@ import { DoneAll as ConfirmIcon, ArrowBack as ArrowBackIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { inventoryService } from "../services/warehouseService"; import { InventoryStatus, InventoryCountLineDto } from "../types"; import dayjs from "dayjs"; export default function InventoryCountPage() { + const { t } = useTranslation(); const { id } = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -107,25 +109,25 @@ export default function InventoryCountPage() { const isEditable = inventory.status === InventoryStatus.InProgress; const columns: GridColDef[] = [ - { field: "articleCode", headerName: "Codice Articolo", width: 150 }, + { field: "articleCode", headerName: t("warehouse.inventory.count.columns.articleCode"), width: 150 }, { field: "articleDescription", - headerName: "Descrizione", + headerName: t("warehouse.inventory.count.columns.description"), flex: 1, minWidth: 200, }, - { field: "batchNumber", headerName: "Lotto", width: 120 }, - { field: "locationCode", headerName: "Ubicazione", width: 120 }, + { field: "batchNumber", headerName: t("warehouse.inventory.count.columns.batch"), width: 120 }, + { field: "locationCode", headerName: t("warehouse.inventory.count.columns.location"), width: 120 }, { field: "theoreticalQuantity", - headerName: "Qta Teorica", + headerName: t("warehouse.inventory.count.columns.theoreticalQty"), width: 120, type: "number", valueFormatter: (value) => (value ? Number(value).toFixed(2) : "0"), }, { field: "countedQuantity", - headerName: "Qta Contata", + headerName: t("warehouse.inventory.count.columns.countedQty"), width: 150, type: "number", editable: isEditable, @@ -137,7 +139,7 @@ export default function InventoryCountPage() { }, { field: "difference", - headerName: "Differenza", + headerName: t("warehouse.inventory.count.columns.difference"), width: 120, type: "number", valueGetter: (_value, row) => { @@ -179,10 +181,10 @@ export default function InventoryCountPage() { startIcon={} onClick={() => navigate("/warehouse/inventory")} > - Indietro + {t("warehouse.inventory.count.actions.back")} - Inventario: {inventory.description} + {t("warehouse.inventory.count.title", { description: inventory.description })} } onClick={() => startMutation.mutate()} > - Avvia Inventario + {t("warehouse.inventory.count.actions.start")} )} {inventory.status === InventoryStatus.InProgress && ( @@ -213,7 +215,7 @@ export default function InventoryCountPage() { startIcon={} onClick={() => completeMutation.mutate()} > - Completa Conteggio + {t("warehouse.inventory.count.actions.complete")} )} {inventory.status === InventoryStatus.Completed && ( @@ -223,7 +225,7 @@ export default function InventoryCountPage() { startIcon={} onClick={() => setConfirmDialogOpen(true)} > - Conferma e Rettifica + {t("warehouse.inventory.count.actions.confirm")} )} @@ -234,7 +236,7 @@ export default function InventoryCountPage() { - Data Inventario + {t("warehouse.inventory.count.cards.date")} {dayjs(inventory.inventoryDate).format("DD/MM/YYYY")} @@ -246,7 +248,7 @@ export default function InventoryCountPage() { - Magazzino + {t("warehouse.inventory.count.cards.warehouse")} {inventory.warehouseName} @@ -256,7 +258,7 @@ export default function InventoryCountPage() { - Righe Totali + {t("warehouse.inventory.count.cards.totalLines")} {inventory.lineCount} @@ -266,7 +268,7 @@ export default function InventoryCountPage() { - Righe Contate + {t("warehouse.inventory.count.cards.countedLines")} {inventory.lines.filter((l) => l.countedQuantity !== null).length} @@ -278,8 +280,7 @@ export default function InventoryCountPage() { {inventory.status === InventoryStatus.Completed && ( - L'inventario è completato. Verifica le differenze prima di confermare. - La conferma genererà automaticamente i movimenti di rettifica. + {t("warehouse.inventory.count.alert.completed")} )} @@ -308,23 +309,21 @@ export default function InventoryCountPage() { open={confirmDialogOpen} onClose={() => setConfirmDialogOpen(false)} > - Conferma Inventario + {t("warehouse.inventory.count.confirmDialog.title")} - Sei sicuro di voler confermare l'inventario? Questa operazione è - irreversibile e genererà i movimenti di rettifica per le differenze - riscontrate. + {t("warehouse.inventory.count.confirmDialog.content")} - + diff --git a/frontend/src/modules/warehouse/pages/InventoryFormPage.tsx b/frontend/src/modules/warehouse/pages/InventoryFormPage.tsx index 47fff61..cc647c6 100644 --- a/frontend/src/modules/warehouse/pages/InventoryFormPage.tsx +++ b/frontend/src/modules/warehouse/pages/InventoryFormPage.tsx @@ -17,6 +17,7 @@ import { CircularProgress, } from "@mui/material"; import { Save as SaveIcon, ArrowBack as ArrowBackIcon } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { inventoryService, warehouseLocationService, @@ -29,6 +30,7 @@ import { import dayjs from "dayjs"; export default function InventoryFormPage() { + const { t } = useTranslation(); const { id } = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -111,7 +113,7 @@ export default function InventoryFormPage() { Inventari - {isEditing ? "Modifica Inventario" : "Nuovo Inventario"} + {isEditing ? t("warehouse.inventory.form.title.edit") : t("warehouse.inventory.form.title.new")} @@ -124,13 +126,13 @@ export default function InventoryFormPage() { }} > - {isEditing ? `Inventario ${inventory?.code}` : "Nuovo Inventario"} + {isEditing ? t("warehouse.inventory.form.title.editWithCode", { code: inventory?.code }) : t("warehouse.inventory.form.title.new")} @@ -139,7 +141,7 @@ export default function InventoryFormPage() { - Magazzino + {t("warehouse.inventory.form.fields.warehouse")} setFormData({ ...formData, @@ -197,7 +199,7 @@ export default function InventoryFormPage() { } > - Tutte + {t("warehouse.inventory.form.options.allCategories")} {categories.map((c) => ( @@ -209,10 +211,10 @@ export default function InventoryFormPage() { - Tipo Inventario + {t("warehouse.inventory.form.fields.type")} } disabled={createMutation.isPending} > - {isEditing ? "Salva Modifiche" : "Crea e Inizia"} + {isEditing ? t("warehouse.inventory.form.actions.save") : t("warehouse.inventory.form.actions.create")} diff --git a/frontend/src/modules/warehouse/pages/InventoryListPage.tsx b/frontend/src/modules/warehouse/pages/InventoryListPage.tsx index 2b8ab57..c8d5192 100644 --- a/frontend/src/modules/warehouse/pages/InventoryListPage.tsx +++ b/frontend/src/modules/warehouse/pages/InventoryListPage.tsx @@ -22,11 +22,13 @@ import { PlayArrow as StartIcon, Cancel as CancelIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { inventoryService } from "../services/warehouseService"; import { InventoryCountDto, InventoryStatus } from "../types"; import dayjs from "dayjs"; export default function InventoryListPage() { + const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [statusFilter] = useState( @@ -58,44 +60,45 @@ export default function InventoryListPage() { }; const getStatusChip = (status: InventoryStatus) => { + const label = t(`warehouse.inventory.status.${InventoryStatus[status]}`); switch (status) { case InventoryStatus.Draft: - return ; + return ; case InventoryStatus.InProgress: - return ; + return ; case InventoryStatus.Completed: - return ; + return ; case InventoryStatus.Confirmed: - return ; + return ; case InventoryStatus.Cancelled: - return ; + return ; default: - return ; + return ; } }; const columns: GridColDef[] = [ - { field: "code", headerName: "Codice", width: 120 }, - { field: "description", headerName: "Descrizione", flex: 1, minWidth: 200 }, + { field: "code", headerName: t("warehouse.inventory.columns.code"), width: 120 }, + { field: "description", headerName: t("warehouse.inventory.columns.description"), flex: 1, minWidth: 200 }, { field: "inventoryDate", - headerName: "Data Inventario", + headerName: t("warehouse.inventory.columns.date"), width: 150, valueFormatter: (value) => value ? dayjs(value).format("DD/MM/YYYY") : "", }, - { field: "warehouseName", headerName: "Magazzino", width: 180 }, - { field: "categoryName", headerName: "Categoria", width: 150 }, + { field: "warehouseName", headerName: t("warehouse.inventory.columns.warehouse"), width: 180 }, + { field: "categoryName", headerName: t("warehouse.inventory.columns.category"), width: 150 }, { field: "status", - headerName: "Stato", + headerName: t("warehouse.inventory.columns.status"), width: 120, renderCell: (params: GridRenderCellParams) => getStatusChip(params.row.status), }, { field: "progress", - headerName: "Progresso", + headerName: t("warehouse.inventory.columns.progress"), width: 150, valueGetter: (_value, row) => { if (!row.lineCount) return "0%"; @@ -107,18 +110,18 @@ export default function InventoryListPage() { }, { field: "actions", - headerName: "Azioni", + headerName: t("warehouse.inventory.columns.actions"), width: 180, sortable: false, renderCell: (params: GridRenderCellParams) => ( - + handleView(params.row.id)}> {params.row.status === InventoryStatus.Draft && ( - + )} {params.row.status === InventoryStatus.InProgress && ( - + )} {params.row.status === InventoryStatus.Draft && ( - + { - if (confirm("Sei sicuro di voler annullare questo inventario?")) { + if (confirm(t("warehouse.inventory.confirmCancel"))) { cancelMutation.mutate(params.row.id); } }} @@ -169,13 +172,13 @@ export default function InventoryListPage() { mb: 3, }} > - Inventari Fisici + {t("warehouse.inventory.title")} diff --git a/frontend/src/modules/warehouse/pages/MovementsPage.tsx b/frontend/src/modules/warehouse/pages/MovementsPage.tsx index be66c5e..ac1b71d 100644 --- a/frontend/src/modules/warehouse/pages/MovementsPage.tsx +++ b/frontend/src/modules/warehouse/pages/MovementsPage.tsx @@ -40,6 +40,7 @@ import { Build as AdjustmentIcon, FilterList as FilterIcon, } from "@mui/icons-material"; +import { useTranslation, Trans } from "react-i18next"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; @@ -66,6 +67,7 @@ import { } from "../types"; export default function MovementsPage() { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const [warehouseId, setWarehouseId] = useState(""); const [movementType, setMovementType] = useState(""); @@ -183,7 +185,7 @@ export default function MovementsPage() { const columns: GridColDef[] = [ { field: "documentNumber", - headerName: "Documento", + headerName: t("warehouse.movements.columns.document"), width: 140, renderCell: (params: GridRenderCellParams) => ( @@ -193,44 +195,44 @@ export default function MovementsPage() { }, { field: "movementDate", - headerName: "Data", + headerName: t("warehouse.movements.columns.date"), width: 110, renderCell: (params: GridRenderCellParams) => formatDate(params.value), }, { field: "type", - headerName: "Tipo", + headerName: t("warehouse.movements.columns.type"), width: 130, renderCell: (params: GridRenderCellParams) => ( ), }, { field: "status", - headerName: "Stato", + headerName: t("warehouse.movements.columns.status"), width: 120, renderCell: (params: GridRenderCellParams) => ( ) => params.row.sourceWarehouseName || @@ -249,7 +251,7 @@ export default function MovementsPage() { }, { field: "destinationWarehouseName", - headerName: "Destinazione", + headerName: t("warehouse.movements.columns.destination"), width: 150, renderCell: (params: GridRenderCellParams) => { // Show destination only for transfers @@ -261,33 +263,33 @@ export default function MovementsPage() { }, { field: "reasonDescription", - headerName: "Causale", + headerName: t("warehouse.movements.columns.reason"), width: 150, renderCell: (params: GridRenderCellParams) => params.value || "-", }, { field: "lineCount", - headerName: "Righe", + headerName: t("warehouse.movements.columns.lines"), width: 80, align: "center", }, { field: "totalValue", - headerName: "Valore", + headerName: t("warehouse.movements.columns.value"), width: 100, align: "right", renderCell: (params: GridRenderCellParams) => params.value != null ? new Intl.NumberFormat("it-IT", { - style: "currency", - currency: "EUR", - }).format(params.value) + style: "currency", + currency: "EUR", + }).format(params.value) : "-", }, { field: "externalReference", - headerName: "Riferimento", + headerName: t("warehouse.movements.columns.reference"), width: 140, renderCell: (params: GridRenderCellParams) => params.value || "-", @@ -306,16 +308,16 @@ export default function MovementsPage() { ]; const speedDialActions = [ - { icon: , name: "Carico", action: nav.goToNewInbound }, - { icon: , name: "Scarico", action: nav.goToNewOutbound }, + { icon: , name: t("warehouse.movements.actions.inbound"), action: nav.goToNewInbound }, + { icon: , name: t("warehouse.movements.actions.outbound"), action: nav.goToNewOutbound }, { icon: , - name: "Trasferimento", + name: t("warehouse.movements.actions.transfer"), action: nav.goToNewTransfer, }, { icon: , - name: "Rettifica", + name: t("warehouse.movements.actions.adjustment"), action: nav.goToNewAdjustment, }, ]; @@ -324,7 +326,7 @@ export default function MovementsPage() { return ( - Errore nel caricamento dei movimenti: {(error as Error).message} + {t("warehouse.movements.loadingError", { error: (error as Error).message })} ); @@ -343,7 +345,7 @@ export default function MovementsPage() { }} > - Movimenti di Magazzino + {t("warehouse.movements.title")} @@ -354,7 +356,7 @@ export default function MovementsPage() { setSearch(e.target.value)} slotProps={{ @@ -377,16 +379,16 @@ export default function MovementsPage() { - Magazzino + {t("warehouse.movements.filters.warehouse")} setMovementType(e.target.value as MovementType | "") } > - Tutti + {t("warehouse.movements.filters.all")} - {Object.entries(movementTypeLabels).map(([value, label]) => ( + {Object.entries(movementTypeLabels).map(([value]) => ( - {label} + {t(`warehouse.movementType.${MovementType[parseInt(value, 10)]}`)} ))} @@ -419,21 +421,21 @@ export default function MovementsPage() { - Stato + {t("warehouse.movements.filters.status")} setWarehouseId(e.target.value as number)} > {warehouses?.map((w) => ( @@ -261,25 +262,25 @@ export default function OutboundMovementPage() { setDocumentNumber(e.target.value)} - placeholder="DDT, Bolla, etc." + placeholder={t("warehouse.outbound.placeholders.documentNumber")} /> setExternalReference(e.target.value)} - placeholder="Ordine, Cliente, etc." + placeholder={t("warehouse.outbound.placeholders.externalReference")} /> setNotes(e.target.value)} multiline @@ -299,9 +300,9 @@ export default function OutboundMovementPage() { mb: 2, }} > - Righe Movimento + {t("warehouse.outbound.sections.lines")} @@ -315,14 +316,14 @@ export default function OutboundMovementPage() {
- Articolo + {t("warehouse.outbound.fields.article")} - Disponibile + {t("warehouse.outbound.fields.available")} - Quantità + {t("warehouse.outbound.fields.quantity")} - Note + {t("warehouse.outbound.fields.notes")} @@ -351,7 +352,7 @@ export default function OutboundMovementPage() { )} isOptionEqualToValue={(option, value) => @@ -401,7 +402,7 @@ export default function OutboundMovementPage() { fullWidth /> {isOverStock && ( - + handleLineChange(line.id, "notes", e.target.value) @@ -443,7 +444,7 @@ export default function OutboundMovementPage() { - Totale Quantità + {t("warehouse.outbound.totals.quantity")} {totalQuantity.toFixed(2)} @@ -452,7 +453,7 @@ export default function OutboundMovementPage() { {/* Actions */} - + diff --git a/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx b/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx index 9cfb205..88391b0 100644 --- a/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx +++ b/frontend/src/modules/warehouse/pages/StockLevelsPage.tsx @@ -25,6 +25,7 @@ import { Warning as WarningIcon, TrendingUp as TrendingUpIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { useStockLevels, useWarehouses, useCategoryTree } from "../hooks"; import { useStockCalculations } from "../hooks/useStockCalculations"; @@ -32,6 +33,7 @@ import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation"; import { StockLevelDto, formatCurrency, formatQuantity } from "../types"; export default function StockLevelsPage() { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const [warehouseId, setWarehouseId] = useState(""); const [categoryId, setCategoryId] = useState(""); @@ -82,7 +84,7 @@ export default function StockLevelsPage() { const columns: GridColDef[] = [ { field: "articleCode", - headerName: "Codice", + headerName: t("warehouse.stockLevels.columns.code"), width: 120, renderCell: (params: GridRenderCellParams) => ( @@ -92,23 +94,23 @@ export default function StockLevelsPage() { }, { field: "articleDescription", - headerName: "Articolo", + headerName: t("warehouse.stockLevels.columns.article"), flex: 1, minWidth: 200, }, { field: "warehouseName", - headerName: "Magazzino", + headerName: t("warehouse.stockLevels.columns.warehouse"), width: 150, }, { field: "categoryName", - headerName: "Categoria", + headerName: t("warehouse.stockLevels.columns.category"), width: 140, }, { field: "quantity", - headerName: "Giacenza", + headerName: t("warehouse.stockLevels.columns.quantity"), width: 120, align: "right", renderCell: (params: GridRenderCellParams) => { @@ -128,7 +130,7 @@ export default function StockLevelsPage() { }, { field: "reservedQuantity", - headerName: "Riservata", + headerName: t("warehouse.stockLevels.columns.reserved"), width: 100, align: "right", renderCell: (params: GridRenderCellParams) => @@ -136,7 +138,7 @@ export default function StockLevelsPage() { }, { field: "availableQuantity", - headerName: "Disponibile", + headerName: t("warehouse.stockLevels.columns.available"), width: 110, align: "right", renderCell: (params: GridRenderCellParams) => { @@ -155,7 +157,7 @@ export default function StockLevelsPage() { }, { field: "unitCost", - headerName: "Costo Medio", + headerName: t("warehouse.stockLevels.columns.averageCost"), width: 120, align: "right", renderCell: (params: GridRenderCellParams) => @@ -163,7 +165,7 @@ export default function StockLevelsPage() { }, { field: "stockValue", - headerName: "Valore", + headerName: t("warehouse.stockLevels.columns.value"), width: 130, align: "right", renderCell: (params: GridRenderCellParams) => ( @@ -177,7 +179,7 @@ export default function StockLevelsPage() { if (error) { return ( - Errore: {(error as Error).message} + {t("warehouse.stockLevels.error", { error: (error as Error).message })} ); } @@ -194,14 +196,14 @@ export default function StockLevelsPage() { }} > - Giacenze di Magazzino + {t("warehouse.stockLevels.title")} @@ -211,7 +213,7 @@ export default function StockLevelsPage() { - Articoli + {t("warehouse.stockLevels.summary.articles")} {summary.articleCount} @@ -223,7 +225,7 @@ export default function StockLevelsPage() { - Quantità Totale + {t("warehouse.stockLevels.summary.totalQuantity")} {formatQuantity(summary.totalQuantity)} @@ -235,7 +237,7 @@ export default function StockLevelsPage() { - Valore Totale + {t("warehouse.stockLevels.summary.totalValue")} {formatCurrency(summary.totalValue)} @@ -247,7 +249,7 @@ export default function StockLevelsPage() { - Sotto Scorta + {t("warehouse.stockLevels.summary.lowStock")} {summary.lowStockCount + summary.outOfStockCount} @@ -264,7 +266,7 @@ export default function StockLevelsPage() { setSearch(e.target.value)} InputProps={{ @@ -285,14 +287,14 @@ export default function StockLevelsPage() { - Magazzino + {t("warehouse.stockLevels.filters.warehouse")} setCategoryId(e.target.value as number | "")} > - Tutte + {t("warehouse.stockLevels.filters.allCategories")} {flatCategories.map((c) => ( @@ -329,7 +331,7 @@ export default function StockLevelsPage() { onChange={(e) => setLowStockOnly(e.target.checked)} /> } - label="Solo sotto scorta" + label={t("warehouse.stockLevels.filters.lowStockOnly")} /> diff --git a/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx b/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx index 61a1b05..d57affd 100644 --- a/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx +++ b/frontend/src/modules/warehouse/pages/TransferMovementPage.tsx @@ -33,6 +33,7 @@ import { Check as ConfirmIcon, SwapHoriz as TransferIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; @@ -56,6 +57,7 @@ interface MovementLine { } export default function TransferMovementPage() { + const { t } = useTranslation(); const navigate = useNavigate(); const [movementDate, setMovementDate] = useState(dayjs()); const [sourceWarehouseId, setSourceWarehouseId] = useState(""); @@ -120,10 +122,10 @@ export default function TransferMovementPage() { const validate = (): boolean => { const newErrors: Record = {}; if (!sourceWarehouseId) { - newErrors.sourceWarehouseId = "Seleziona magazzino origine"; + newErrors.sourceWarehouseId = t("warehouse.transfer.validation.sourceRequired"); } if (!destWarehouseId) { - newErrors.destWarehouseId = "Seleziona magazzino destinazione"; + newErrors.destWarehouseId = t("warehouse.transfer.validation.destRequired"); } if ( sourceWarehouseId && @@ -131,14 +133,14 @@ export default function TransferMovementPage() { sourceWarehouseId === destWarehouseId ) { newErrors.destWarehouseId = - "Origine e destinazione devono essere diversi"; + t("warehouse.transfer.validation.sameWarehouse"); } if (!movementDate) { - newErrors.movementDate = "Inserisci la data"; + newErrors.movementDate = t("warehouse.transfer.validation.dateRequired"); } const validLines = lines.filter((l) => l.article && l.quantity > 0); if (validLines.length === 0) { - newErrors.lines = "Inserisci almeno una riga"; + newErrors.lines = t("warehouse.transfer.validation.linesRequired"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -188,10 +190,10 @@ export default function TransferMovementPage() { - Trasferimento tra Magazzini + {t("warehouse.transfer.title")} - Sposta merce da un magazzino all'altro + {t("warehouse.transfer.subtitle")} @@ -199,20 +201,19 @@ export default function TransferMovementPage() { {(createMutation.error || confirmMutation.error) && ( - Errore:{" "} - {((createMutation.error || confirmMutation.error) as Error).message} + {t("warehouse.transfer.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })} )} {/* Form */} - Dati Trasferimento + {t("warehouse.transfer.sections.transferData")} - Magazzino Origine + {t("warehouse.transfer.fields.sourceWarehouse")} setDestWarehouseId(e.target.value as number)} > {warehouses @@ -275,7 +276,7 @@ export default function TransferMovementPage() { setDocumentNumber(e.target.value)} /> @@ -283,7 +284,7 @@ export default function TransferMovementPage() { setExternalReference(e.target.value)} /> @@ -291,7 +292,7 @@ export default function TransferMovementPage() { setNotes(e.target.value)} /> @@ -302,9 +303,9 @@ export default function TransferMovementPage() { {/* Lines */} - Articoli da Trasferire + {t("warehouse.transfer.sections.lines")} @@ -318,10 +319,10 @@ export default function TransferMovementPage() {
- Articolo - Disponibile - Quantità - Note + {t("warehouse.transfer.fields.article")} + {t("warehouse.transfer.fields.available")} + {t("warehouse.transfer.fields.quantity")} + {t("warehouse.transfer.fields.notes")} @@ -340,7 +341,7 @@ export default function TransferMovementPage() { )} isOptionEqualToValue={(o, v) => o.id === v.id} @@ -389,7 +390,7 @@ export default function TransferMovementPage() { onChange={(e) => handleLineChange(line.id, "notes", e.target.value) } - placeholder="Note" + placeholder={t("warehouse.transfer.placeholders.notes")} fullWidth /> @@ -411,14 +412,14 @@ export default function TransferMovementPage() { - Totale: {formatQuantity(totalQuantity)} + {t("warehouse.transfer.totals.total", { value: formatQuantity(totalQuantity) })} {/* Actions */} - + diff --git a/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx b/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx index f7ef359..0ff837c 100644 --- a/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx +++ b/frontend/src/modules/warehouse/pages/WarehouseDashboard.tsx @@ -27,6 +27,7 @@ import { Add as AddIcon, Assessment as AssessmentIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { useArticles, useWarehouses, @@ -107,6 +108,7 @@ function StatCard({ export default function WarehouseDashboard() { const nav = useWarehouseNavigation(); + const { t } = useTranslation(); const { data: articles, isLoading: loadingArticles } = useArticles({ isActive: true, @@ -189,14 +191,14 @@ export default function WarehouseDashboard() { startIcon={} onClick={nav.goToNewInbound} > - Nuovo Carico + {t("warehouse.dashboard.newInbound")} @@ -204,7 +206,7 @@ export default function WarehouseDashboard() { } loading={loadingArticles} @@ -212,7 +214,7 @@ export default function WarehouseDashboard() { } loading={loadingWarehouses} @@ -220,7 +222,7 @@ export default function WarehouseDashboard() { } color="success.main" @@ -229,9 +231,9 @@ export default function WarehouseDashboard() { } color="warning.main" loading={loadingStock} @@ -252,13 +254,13 @@ export default function WarehouseDashboard() { mb: 2, }} > - Ultimi Movimenti + {t("warehouse.dashboard.recentMovements")} {loadingMovements ? ( @@ -269,7 +271,7 @@ export default function WarehouseDashboard() { ) : lastMovements.length === 0 ? ( - Nessun movimento recente + {t("warehouse.dashboard.noRecentMovements")} ) : ( @@ -317,7 +319,7 @@ export default function WarehouseDashboard() { secondary={`${movement.sourceWarehouseName || movement.destinationWarehouseName || "-"} - ${formatDate(movement.movementDate)}`} /> - {formatQuantity(movement.lineCount)} righe + {formatQuantity(movement.lineCount)} {t("warehouse.dashboard.lines")} {index < lastMovements.length - 1 && } @@ -339,12 +341,11 @@ export default function WarehouseDashboard() { icon={} action={ } > - {pendingMovements.length} movimenti in bozza - da confermare + {pendingMovements.length} {t("warehouse.dashboard.draftMovements")} )} @@ -357,12 +358,11 @@ export default function WarehouseDashboard() { icon={} action={ } > - {expiringBatches.length} lotti in scadenza - nei prossimi 30 giorni + {expiringBatches.length} {t("warehouse.dashboard.expiringBatches")} )} @@ -378,18 +378,18 @@ export default function WarehouseDashboard() { mb: 2, }} > - Articoli Sotto Scorta + {t("warehouse.dashboard.lowStockArticles")} {lowStockArticles.length === 0 ? ( - Nessun articolo sotto scorta + {t("warehouse.dashboard.noLowStockArticles")} ) : ( @@ -429,7 +429,7 @@ export default function WarehouseDashboard() { - Azioni Rapide + {t("warehouse.dashboard.quickActions")} @@ -443,7 +443,7 @@ export default function WarehouseDashboard() { > - Carico + {t("warehouse.dashboard.inbound")} @@ -458,7 +458,7 @@ export default function WarehouseDashboard() { > - Scarico + {t("warehouse.dashboard.outbound")} @@ -473,7 +473,7 @@ export default function WarehouseDashboard() { > - Trasferimento + {t("warehouse.dashboard.transfer")} @@ -488,7 +488,7 @@ export default function WarehouseDashboard() { > - Nuovo Articolo + {t("warehouse.dashboard.newArticle")} @@ -503,7 +503,7 @@ export default function WarehouseDashboard() { > - Inventario + {t("warehouse.dashboard.inventory")} @@ -518,7 +518,7 @@ export default function WarehouseDashboard() { > - Valorizzazione + {t("warehouse.dashboard.valuation")} diff --git a/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx b/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx index e96a4a1..69994c1 100644 --- a/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx +++ b/frontend/src/modules/warehouse/pages/WarehouseLocationsPage.tsx @@ -32,6 +32,7 @@ import { StarBorder as StarBorderIcon, Warehouse as WarehouseIcon, } from "@mui/icons-material"; +import { useTranslation, Trans } from "react-i18next"; import { useWarehouses, useCreateWarehouse, @@ -58,6 +59,7 @@ const initialFormData = { }; export default function WarehouseLocationsPage() { + const { t } = useTranslation(); const [dialogOpen, setDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [editingWarehouse, setEditingWarehouse] = @@ -113,7 +115,7 @@ export default function WarehouseLocationsPage() { const newErrors: Record = {}; // Il codice è generato automaticamente, non richiede validazione in creazione if (!formData.name.trim()) { - newErrors.name = "Il nome è obbligatorio"; + newErrors.name = t("warehouse.locations.dialog.validation.nameRequired"); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -183,7 +185,7 @@ export default function WarehouseLocationsPage() { return ( - Errore nel caricamento dei magazzini: {(error as Error).message} + {t("warehouse.locations.loadingError", { error: (error as Error).message })} ); @@ -201,14 +203,14 @@ export default function WarehouseLocationsPage() { }} > - Gestione Magazzini + {t("warehouse.locations.title")} @@ -231,7 +233,7 @@ export default function WarehouseLocationsPage() { - Nessun magazzino configurato + {t("warehouse.locations.emptyState.title")} @@ -256,7 +258,7 @@ export default function WarehouseLocationsPage() { }} > {warehouse.isDefault && ( - + {!warehouse.isActive && ( - + )} {warehouse.address && ( @@ -317,7 +319,7 @@ export default function WarehouseLocationsPage() { )} - + handleSetDefault(warehouse)} @@ -332,7 +334,7 @@ export default function WarehouseLocationsPage() { )} - + handleOpenDialog(warehouse)} @@ -340,7 +342,7 @@ export default function WarehouseLocationsPage() { - + handleDeleteClick(warehouse)} @@ -364,22 +366,22 @@ export default function WarehouseLocationsPage() { fullWidth > - {editingWarehouse ? "Modifica Magazzino" : "Nuovo Magazzino"} + {editingWarehouse ? t("warehouse.locations.dialog.editTitle") : t("warehouse.locations.dialog.createTitle")} @@ -399,18 +401,18 @@ export default function WarehouseLocationsPage() { handleChange("alternativeCode", e.target.value) } - helperText="Opzionale" + helperText={t("warehouse.locations.dialog.helpers.optional")} /> handleChange("name", e.target.value)} error={!!errors.name} @@ -421,7 +423,7 @@ export default function WarehouseLocationsPage() { handleChange("description", e.target.value)} multiline @@ -430,15 +432,15 @@ export default function WarehouseLocationsPage() { - Tipo + {t("warehouse.locations.dialog.fields.type")} @@ -447,7 +449,7 @@ export default function WarehouseLocationsPage() { handleChange("address", e.target.value)} /> @@ -462,7 +464,7 @@ export default function WarehouseLocationsPage() { } /> } - label="Magazzino Predefinito" + label={t("warehouse.locations.dialog.fields.isDefault")} /> @@ -473,21 +475,21 @@ export default function WarehouseLocationsPage() { onChange={(e) => handleChange("isActive", e.target.checked)} /> } - label="Attivo" + label={t("warehouse.locations.dialog.fields.isActive")} /> - + @@ -497,28 +499,28 @@ export default function WarehouseLocationsPage() { open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)} > - Conferma Eliminazione + {t("warehouse.locations.deleteDialog.title")} - Sei sicuro di voler eliminare il magazzino{" "} - - {warehouseToDelete?.code} - {warehouseToDelete?.name} - - ? + }} + /> - Questa azione non può essere annullata. + {t("warehouse.locations.deleteDialog.warning")} - + diff --git a/frontend/src/pages/ArticoliPage.tsx b/frontend/src/pages/ArticoliPage.tsx index 82d4c8a..d37a07a 100644 --- a/frontend/src/pages/ArticoliPage.tsx +++ b/frontend/src/pages/ArticoliPage.tsx @@ -23,11 +23,13 @@ import { Edit as EditIcon, Delete as DeleteIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { articoliService, lookupService } from "../services/lookupService"; import { Articolo } from "../types"; export default function ArticoliPage() { const queryClient = useQueryClient(); + const { t } = useTranslation(); const [openDialog, setOpenDialog] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState>({ attivo: true }); @@ -94,34 +96,34 @@ export default function ArticoliPage() { }; const columns: GridColDef[] = [ - { field: "codice", headerName: "Codice", width: 100 }, - { field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 }, - { field: "descrizione", headerName: "Descrizione", flex: 1, minWidth: 200 }, + { field: "codice", headerName: t("articles.code"), width: 100 }, + { field: "codiceAlternativo", headerName: t("articles.altCode"), width: 100 }, + { field: "descrizione", headerName: t("articles.description"), flex: 1, minWidth: 200 }, { field: "tipoMateriale", - headerName: "Tipo", + headerName: t("articles.type"), width: 130, valueGetter: (value: any) => value?.descrizione || "", }, { field: "categoria", - headerName: "Categoria", + headerName: t("articles.category"), width: 120, valueGetter: (value: any) => value?.descrizione || "", }, { field: "qtaDisponibile", - headerName: "Disponibile", + headerName: t("articles.available"), width: 100, type: "number", }, - { field: "qtaStdA", headerName: "Qta A", width: 80, type: "number" }, - { field: "qtaStdB", headerName: "Qta B", width: 80, type: "number" }, - { field: "qtaStdS", headerName: "Qta S", width: 80, type: "number" }, - { field: "unitaMisura", headerName: "UM", width: 60 }, + { field: "qtaStdA", headerName: t("articles.qtyA"), width: 80, type: "number" }, + { field: "qtaStdB", headerName: t("articles.qtyB"), width: 80, type: "number" }, + { field: "qtaStdS", headerName: t("articles.qtyS"), width: 80, type: "number" }, + { field: "unitaMisura", headerName: t("articles.uom"), width: 60 }, { field: "actions", - headerName: "Azioni", + headerName: t("common.actions"), width: 120, sortable: false, renderCell: (params) => ( @@ -133,7 +135,7 @@ export default function ArticoliPage() { size="small" color="error" onClick={() => { - if (confirm("Eliminare questo articolo?")) { + if (confirm(t("articles.deleteConfirm"))) { deleteMutation.mutate(params.row.id); } }} @@ -155,13 +157,13 @@ export default function ArticoliPage() { mb: 2, }} > - Articoli + {t("articles.title")} @@ -185,24 +187,24 @@ export default function ArticoliPage() { fullWidth > - {editingId ? "Modifica Articolo" : "Nuovo Articolo"} + {editingId ? t("articles.editArticle") : t("articles.newArticle")} @@ -230,12 +232,12 @@ export default function ArticoliPage() { codiceAlternativo: e.target.value, }) } - helperText="Opzionale" + helperText={t("common.optional")} /> - Tipo Materiale + {t("articles.materialType")} setFormData({ ...formData, @@ -288,7 +290,7 @@ export default function ArticoliPage() { @@ -313,7 +315,7 @@ export default function ArticoliPage() { - + diff --git a/frontend/src/pages/AutoCodesAdminPage.tsx b/frontend/src/pages/AutoCodesAdminPage.tsx index 1a1b1d1..327c64f 100644 --- a/frontend/src/pages/AutoCodesAdminPage.tsx +++ b/frontend/src/pages/AutoCodesAdminPage.tsx @@ -47,6 +47,7 @@ import { ContentCopy as CopyIcon, } from "@mui/icons-material"; import * as Icons from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { autoCodeService } from "../services/autoCodeService"; import type { AutoCodeDto, @@ -57,6 +58,7 @@ import { groupByModule, moduleNames, moduleIcons } from "../types/autoCode"; export default function AutoCodesAdminPage() { const queryClient = useQueryClient(); + const { t } = useTranslation(); const [editingConfig, setEditingConfig] = useState(null); const [confirmReset, setConfirmReset] = useState(null); const [previewCode, setPreviewCode] = useState(null); @@ -142,10 +144,10 @@ export default function AutoCodesAdminPage() { > - Codici Automatici + {t("autoCodes.title")} - Configura i pattern per la generazione automatica dei codici + {t("autoCodes.subtitle")} @@ -154,14 +156,14 @@ export default function AutoCodesAdminPage() { startIcon={} onClick={() => setShowHelp(true)} > - Guida Pattern + {t("autoCodes.helpPattern")} @@ -194,14 +196,14 @@ export default function AutoCodesAdminPage() {
- Entita - Prefisso - Pattern - Esempio - Sequenza - Reset - Stato - Azioni + {t("autoCodes.entity")} + {t("autoCodes.prefix")} + {t("autoCodes.pattern")} + {t("autoCodes.example")} + {t("autoCodes.sequence")} + {t("autoCodes.reset")} + {t("autoCodes.status")} + {t("common.actions")} @@ -250,7 +252,7 @@ export default function AutoCodesAdminPage() { > {config.exampleCode} - + @@ -270,22 +272,22 @@ export default function AutoCodesAdminPage() { {config.resetSequenceMonthly ? ( - + ) : config.resetSequenceYearly ? ( - + ) : ( - + )} - + setEditingConfig(config)} @@ -293,7 +295,7 @@ export default function AutoCodesAdminPage() { - + setConfirmReset(config.entityCode)} @@ -324,6 +326,7 @@ export default function AutoCodesAdminPage() { }} isSaving={updateMutation.isPending} error={updateMutation.error as Error | null} + t={t} /> {/* Dialog conferma reset */} @@ -333,22 +336,21 @@ export default function AutoCodesAdminPage() { maxWidth="xs" fullWidth > - Conferma Reset Sequenza + {t("autoCodes.resetConfirmTitle")} - Sei sicuro di voler resettare la sequenza per{" "} + {t("autoCodes.resetConfirmText")}{" "} {configs.find((c) => c.entityCode === confirmReset)?.entityName} ? - La sequenza verra riportata a 0. Il prossimo codice generato partira - da 1. + {t("autoCodes.resetWarning")} - + @@ -374,7 +376,7 @@ export default function AutoCodesAdminPage() { maxWidth="xs" fullWidth > - Anteprima Prossimo Codice + {t("autoCodes.previewTitle")} - Questo e il codice che verra generato alla prossima creazione. -
- La sequenza non e stata incrementata. + {t("autoCodes.previewText")}
- + @@ -427,15 +427,14 @@ export default function AutoCodesAdminPage() { maxWidth="md" fullWidth > - Guida ai Pattern + {t("autoCodes.helpTitle")} - I pattern definiscono come vengono generati i codici automatici. - Puoi combinare testo statico e placeholder dinamici. + {t("autoCodes.helpText")} - Placeholder Disponibili + {t("autoCodes.placeholders")}
@@ -467,7 +466,7 @@ export default function AutoCodesAdminPage() { - Esempi di Pattern + {t("autoCodes.examples")} @@ -533,7 +532,7 @@ export default function AutoCodesAdminPage() { - + @@ -548,6 +547,7 @@ interface EditConfigDialogProps { onSave: (data: AutoCodeUpdateDto) => void; isSaving: boolean; error: Error | null; + t: (key: string) => string; } function EditConfigDialog({ @@ -557,6 +557,7 @@ function EditConfigDialog({ onSave, isSaving, error, + t, }: EditConfigDialogProps) { const [formData, setFormData] = useState({}); @@ -610,33 +611,33 @@ function EditConfigDialog({ {config && ( <> - Modifica Configurazione: {config.entityName} + {t("autoCodes.editTitle")}: {config.entityName} setFormData({ ...formData, prefix: e.target.value || null }) } fullWidth size="small" - helperText="Testo sostituito nel placeholder {PREFIX}" + helperText={t("autoCodes.prefixHelper")} /> setFormData({ ...formData, pattern: e.target.value }) } fullWidth size="small" - helperText="Pattern per generazione codice" + helperText={t("autoCodes.patternHelper")} InputProps={{ sx: { fontFamily: "monospace" }, endAdornment: ( @@ -674,7 +675,7 @@ function EditConfigDialog({ }} > - Anteprima: + {t("autoCodes.previewLabel")} - Reset Sequenza + {t("autoCodes.resetSequence")} @@ -745,7 +746,7 @@ function EditConfigDialog({ } /> } - label="Generazione attiva" + label={t("autoCodes.generationActive")} /> @@ -762,19 +763,19 @@ function EditConfigDialog({ } /> } - label="Codice non modificabile" + label={t("autoCodes.readOnly")} /> {error && ( - {error.message || "Errore durante il salvataggio"} + {error.message || t("common.error")} )} - + diff --git a/frontend/src/pages/CalendarioPage.tsx b/frontend/src/pages/CalendarioPage.tsx index 5cde3c7..0fb98aa 100644 --- a/frontend/src/pages/CalendarioPage.tsx +++ b/frontend/src/pages/CalendarioPage.tsx @@ -17,10 +17,12 @@ import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import { useNavigate } from "react-router-dom"; import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; import { eventiService } from "../services/eventiService"; export default function CalendarioPage() { const navigate = useNavigate(); + const { t, i18n } = useTranslation(); const [dateRange, setDateRange] = useState({ start: dayjs().startOf("month").format("YYYY-MM-DD"), end: dayjs().endOf("month").format("YYYY-MM-DD"), @@ -73,7 +75,7 @@ export default function CalendarioPage() { return ( - Calendario Eventi + {t("calendar.title")} @@ -85,7 +87,7 @@ export default function CalendarioPage() { center: "title", right: "dayGridMonth,timeGridWeek,timeGridDay", }} - locale="it" + locale={i18n.language} events={eventi.map((e) => ({ id: String(e.id), title: e.title, @@ -109,27 +111,27 @@ export default function CalendarioPage() { meridiem: false, }} buttonText={{ - today: "Oggi", - month: "Mese", - week: "Settimana", - day: "Giorno", + today: t("calendar.today"), + month: t("calendar.month"), + week: t("calendar.week"), + day: t("calendar.day"), }} /> {/* Dialog per creazione nuovo evento */} - Nuovo Evento + {t("calendar.newEvent")} - Vuoi creare un nuovo evento per il giorno{" "} + {t("calendar.createEventConfirm")}{" "} {newEventDialog.formattedDate}? - + diff --git a/frontend/src/pages/ClientiPage.tsx b/frontend/src/pages/ClientiPage.tsx index 19a26de..0d53832 100644 --- a/frontend/src/pages/ClientiPage.tsx +++ b/frontend/src/pages/ClientiPage.tsx @@ -19,6 +19,7 @@ import { Edit as EditIcon, Delete as DeleteIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { clientiService } from "../services/lookupService"; import { Cliente } from "../types"; import { CustomFieldsRenderer } from "../components/customFields/CustomFieldsRenderer"; @@ -26,6 +27,7 @@ import { CustomFieldValues } from "../types/customFields"; export default function ClientiPage() { const queryClient = useQueryClient(); + const { t } = useTranslation(); const [openDialog, setOpenDialog] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState>({ attivo: true }); @@ -94,22 +96,22 @@ export default function ClientiPage() { }; const columns: GridColDef[] = [ - { field: "codice", headerName: "Codice", width: 100 }, - { field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 }, + { field: "codice", headerName: t("clients.code"), width: 100 }, + { field: "codiceAlternativo", headerName: t("clients.altCode"), width: 100 }, { field: "ragioneSociale", - headerName: "Ragione Sociale", + headerName: t("clients.businessName"), flex: 1, minWidth: 200, }, - { field: "citta", headerName: "Città", width: 150 }, - { field: "provincia", headerName: "Prov.", width: 80 }, - { field: "telefono", headerName: "Telefono", width: 130 }, - { field: "email", headerName: "Email", width: 200 }, - { field: "partitaIva", headerName: "P.IVA", width: 130 }, + { field: "citta", headerName: t("clients.city"), width: 150 }, + { field: "provincia", headerName: t("clients.province"), width: 80 }, + { field: "telefono", headerName: t("clients.phone"), width: 130 }, + { field: "email", headerName: t("clients.email"), width: 200 }, + { field: "partitaIva", headerName: t("clients.vat"), width: 130 }, { field: "actions", - headerName: "Azioni", + headerName: t("common.actions"), width: 120, sortable: false, renderCell: (params) => ( @@ -121,7 +123,7 @@ export default function ClientiPage() { size="small" color="error" onClick={() => { - if (confirm("Eliminare questo cliente?")) { + if (confirm(t("clients.deleteConfirm"))) { deleteMutation.mutate(params.row.id); } }} @@ -143,13 +145,13 @@ export default function ClientiPage() { mb: 2, }} > - Clienti + {t("clients.title")} @@ -173,24 +175,24 @@ export default function ClientiPage() { fullWidth > - {editingId ? "Modifica Cliente" : "Nuovo Cliente"} + {editingId ? t("clients.editClient") : t("clients.newClient")} @@ -218,12 +220,12 @@ export default function ClientiPage() { codiceAlternativo: e.target.value, }) } - helperText="Opzionale" + helperText={t("common.optional")} /> @@ -244,7 +246,7 @@ export default function ClientiPage() { @@ -254,7 +256,7 @@ export default function ClientiPage() { @@ -264,7 +266,7 @@ export default function ClientiPage() { @@ -274,7 +276,7 @@ export default function ClientiPage() { @@ -284,7 +286,7 @@ export default function ClientiPage() { @@ -305,7 +307,7 @@ export default function ClientiPage() { @@ -315,7 +317,7 @@ export default function ClientiPage() { @@ -325,7 +327,7 @@ export default function ClientiPage() { @@ -338,7 +340,7 @@ export default function ClientiPage() { - + diff --git a/frontend/src/pages/CustomFieldsAdminPage.tsx b/frontend/src/pages/CustomFieldsAdminPage.tsx index 84f3061..d3fa1e2 100644 --- a/frontend/src/pages/CustomFieldsAdminPage.tsx +++ b/frontend/src/pages/CustomFieldsAdminPage.tsx @@ -1,302 +1,627 @@ -import React, { useState, useEffect } from 'react'; +import { useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Box, - Button, - Card, - CardContent, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControl, - - InputLabel, - MenuItem, - Select, - TextField, Typography, + Button, + Chip, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Alert, + CircularProgress, + LinearProgress, + Switch, FormControlLabel, - Checkbox, - Snackbar, - Alert -} from '@mui/material'; -import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid'; -import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; -import { CustomFieldDefinition, CustomFieldType } from '../types/customFields'; -import { customFieldService } from '../services/customFieldService'; + TextField, + Accordion, + AccordionSummary, + AccordionDetails, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + FormControl, + InputLabel, + Select, + MenuItem, + Tooltip, +} from "@mui/material"; +import Grid from "@mui/material/Grid"; +import { + Refresh as RefreshIcon, + Edit as EditIcon, + Delete as DeleteIcon, + ExpandMore as ExpandMoreIcon, + Add as AddIcon, +} from "@mui/icons-material"; +import * as Icons from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { customFieldService } from "../services/customFieldService"; +import { + CustomFieldDefinition, + CustomFieldType, + groupByEntity, + entityNames, + entityIcons, + fieldTypeNames, +} from "../types/customFields"; -const ENTITIES = [ - { value: 'Cliente', label: 'Clienti' }, - { value: 'Articolo', label: 'Articoli (Catering)' }, - { value: 'Evento', label: 'Eventi' }, - { value: 'WarehouseArticle', label: 'Articoli Magazzino' }, - { value: 'WarehouseLocation', label: 'Magazzini' }, - { value: 'Risorsa', label: 'Risorse (Staff)' } -]; +export default function CustomFieldsAdminPage() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + const [editingConfig, setEditingConfig] = useState( + null, + ); + const [isCreating, setIsCreating] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(null); + const [expandedEntity, setExpandedEntity] = useState("Client"); -const FIELD_TYPES = [ - { value: CustomFieldType.Text, label: 'Testo' }, - { value: CustomFieldType.Number, label: 'Numero' }, - { value: CustomFieldType.Date, label: 'Data' }, - { value: CustomFieldType.Boolean, label: 'Booleano (Sì/No)' }, - { value: CustomFieldType.Select, label: 'Lista a discesa' }, - { value: CustomFieldType.TextArea, label: 'Area di testo' }, - { value: CustomFieldType.Color, label: 'Colore' }, - { value: CustomFieldType.Url, label: 'URL' }, - { value: CustomFieldType.Email, label: 'Email' } -]; - -const CustomFieldsAdminPage: React.FC = () => { - const [selectedEntity, setSelectedEntity] = useState(ENTITIES[0].value); - const [fields, setFields] = useState([]); - const [loading, setLoading] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [currentField, setCurrentField] = useState>({}); - const [snackbar, setSnackbar] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({ - open: false, - message: '', - severity: 'success' + // Query per tutte le configurazioni + const { + data: configs = [], + isLoading, + refetch, + } = useQuery({ + queryKey: ["customFields"], + queryFn: () => customFieldService.getAll(), }); - const showSnackbar = (message: string, severity: 'success' | 'error') => { - setSnackbar({ open: true, message, severity }); + // Mutation per creare configurazione + const createMutation = useMutation({ + mutationFn: (data: Omit) => + customFieldService.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customFields"] }); + setIsCreating(false); + }, + }); + + // Mutation per aggiornare configurazione + const updateMutation = useMutation({ + mutationFn: ({ + id, + data, + }: { + id: number; + data: CustomFieldDefinition; + }) => customFieldService.update(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customFields"] }); + setEditingConfig(null); + }, + }); + + // Mutation per eliminare configurazione + const deleteMutation = useMutation({ + mutationFn: (id: number) => customFieldService.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customFields"] }); + setConfirmDelete(null); + }, + }); + + // Raggruppa configurazioni per entità + const groupedConfigs = groupByEntity(configs); + + // Helper per ottenere icona entità + const getEntityIcon = (entityCode: string) => { + const iconName = entityIcons[entityCode] || "Extension"; + const IconComponent = (Icons as Record)[ + iconName + ]; + return IconComponent ? : ; }; - const handleCloseSnackbar = () => setSnackbar({ ...snackbar, open: false }); - - const loadFields = async () => { - setLoading(true); - try { - const data = await customFieldService.getByEntity(selectedEntity); - setFields(data); - } catch (error) { - showSnackbar('Errore nel caricamento dei campi', 'error'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadFields(); - }, [selectedEntity]); - - const handleSave = async () => { - try { - if (currentField.id) { - await customFieldService.update(currentField.id, currentField as CustomFieldDefinition); - showSnackbar('Campo aggiornato con successo', 'success'); - } else { - await customFieldService.create({ - ...currentField, - entityName: selectedEntity, - isActive: true, - sortOrder: fields.length + 1 - } as CustomFieldDefinition); - showSnackbar('Campo creato con successo', 'success'); - } - setDialogOpen(false); - loadFields(); - } catch (error) { - showSnackbar('Errore nel salvataggio', 'error'); - } - }; - - const handleDelete = async (id: number) => { - if (window.confirm('Sei sicuro di voler eliminare questo campo?')) { - try { - await customFieldService.delete(id); - showSnackbar('Campo eliminato', 'success'); - loadFields(); - } catch (error) { - showSnackbar('Errore durante l\'eliminazione', 'error'); - } - } - }; - - const columns: GridColDef[] = [ - { field: 'label', headerName: 'Etichetta', flex: 1 }, - { field: 'fieldName', headerName: 'Nome Interno', flex: 1 }, - { - field: 'type', - headerName: 'Tipo', - width: 150, - valueFormatter: (params) => FIELD_TYPES.find(t => t.value === params.value)?.label - }, - { - field: 'isRequired', - headerName: 'Obbligatorio', - width: 120, - type: 'boolean' - }, - { - field: 'sortOrder', - headerName: 'Ordine', - width: 100, - type: 'number', - editable: true - }, - { - field: 'actions', - type: 'actions', - headerName: 'Azioni', - width: 100, - getActions: (params) => [ - } - label="Modifica" - onClick={() => { - setCurrentField(params.row); - setDialogOpen(true); - }} - />, - } - label="Elimina" - onClick={() => handleDelete(params.row.id)} - />, - ], - }, - ]; + if (isLoading) { + return ( + + + + ); + } return ( - - - Gestione Campi Personalizzati - + + {/* Header */} + + + + {t("customFields.title")} + + + {t("customFields.title")} + + + + + + + - - - - - - Entità -
+ + + {t("customFields.label")} + {t("customFields.fieldName")} + {t("customFields.type")} + + {t("customFields.required")} + + + {t("customFields.order")} + + + {t("autoCodes.status")} + + + {t("common.actions")} + + + + + {entityConfigs.map((config) => ( + + + + {config.label} + + {config.description && ( + + {config.description} + + )} + + + + {config.fieldName} + + + + + + + {config.isRequired ? ( + + ) : ( + + )} + + + {config.sortOrder} + + + + + + + setEditingConfig(config)} + > + + + + + setConfirmDelete(config.id)} + > + + + + + + ))} + +
+
+ )} + + + ); + })} + + {/* Dialog creazione/modifica */} + { + setEditingConfig(null); + setIsCreating(false); + }} + onSave={(data) => { + if (isCreating) { + createMutation.mutate(data as Omit); + } else if (editingConfig) { + updateMutation.mutate({ + id: editingConfig.id, + data: data as CustomFieldDefinition, + }); + } + }} + isSaving={createMutation.isPending || updateMutation.isPending} + error={ + (createMutation.error || updateMutation.error) as Error | null + } + t={t} + /> + + {/* Dialog conferma eliminazione */} + setConfirmDelete(null)} + maxWidth="xs" + fullWidth + > + {t("common.confirmDelete")} + + {t("customFields.deleteConfirm")} + + {t("common.irreversibleAction")} + + + + + + + +
+ ); +} + +// Dialog per creazione/modifica configurazione +interface EditConfigDialogProps { + config: CustomFieldDefinition | null; + isCreating: boolean; + onClose: () => void; + onSave: ( + data: Omit | CustomFieldDefinition, + ) => void; + isSaving: boolean; + error: Error | null; + t: (key: string) => string; +} + +function EditConfigDialog({ + config, + isCreating, + onClose, + onSave, + isSaving, + error, + t, +}: EditConfigDialogProps) { + const [formData, setFormData] = useState>({}); + + // Reset form quando apre + const handleOpen = () => { + if (config) { + setFormData({ + entityName: config.entityName, + fieldName: config.fieldName, + label: config.label, + type: config.type, + isRequired: config.isRequired, + optionsJson: config.optionsJson, + defaultValue: config.defaultValue, + description: config.description, + sortOrder: config.sortOrder, + isActive: config.isActive, + }); + } else { + setFormData({ + entityName: "Client", + type: CustomFieldType.Text, + isRequired: false, + isActive: true, + sortOrder: 0, + }); + } + }; + + const handleSave = () => { + onSave(formData as Omit); + }; + + return ( + + + {isCreating + ? t("customFields.newField") + : `${t("customFields.editField")}: ${config?.label}`} + + + + {isCreating && ( + + + {t("customFields.entity")} + - - - - - - - + + )} -
- -
- - setDialogOpen(false)} maxWidth="sm" fullWidth> - {currentField.id ? 'Modifica Campo' : 'Nuovo Campo'} - - + + setFormData({ ...formData, label: e.target.value }) + } fullWidth - value={currentField.label || ''} - onChange={(e) => setCurrentField({ ...currentField, label: e.target.value })} + size="small" required /> + + + + setFormData({ ...formData, fieldName: e.target.value }) + } fullWidth - value={currentField.fieldName || ''} - onChange={(e) => setCurrentField({ ...currentField, fieldName: e.target.value })} + size="small" required - helperText="Deve essere univoco per l'entità. Usa solo lettere minuscole e underscore." - disabled={!!currentField.id} + disabled={!isCreating} + helperText={isCreating ? t("customFields.fieldNameHelper") : ""} /> - - Tipo + + + + + {t("customFields.type")} + - {currentField.type === CustomFieldType.Select && ( - setCurrentField({ ...currentField, optionsJson: e.target.value })} - placeholder='["Opzione 1", "Opzione 2"]' - helperText="Inserisci un array JSON valido di stringhe" - /> + {(formData.type === CustomFieldType.Select || + formData.type === CustomFieldType.MultiSelect) && ( + + + setFormData({ ...formData, optionsJson: e.target.value }) + } + fullWidth + size="small" + multiline + rows={3} + helperText={t("customFields.optionsHelper")} + InputProps={{ sx: { fontFamily: "monospace" } }} + /> + )} + + setFormData({ ...formData, description: e.target.value }) + } fullWidth - value={currentField.description || ''} - onChange={(e) => setCurrentField({ ...currentField, description: e.target.value })} + size="small" + multiline + rows={2} /> + + + + setFormData({ + ...formData, + sortOrder: parseInt(e.target.value), + }) + } + fullWidth + size="small" + /> + + + setCurrentField({ ...currentField, isRequired: e.target.checked })} + + setFormData({ ...formData, isRequired: e.target.checked }) + } /> } - label="Obbligatorio" + label={t("customFields.required")} /> + - setCurrentField({ ...currentField, sortOrder: parseInt(e.target.value) })} + + + setFormData({ ...formData, isActive: e.target.checked }) + } + /> + } + label={t("modules.admin.active")} /> - - - - - - - +
+
- - - {snackbar.message} - - - + {error && ( + + {error.message || t("common.error")} + + )} + + + + + + ); }; -export default CustomFieldsAdminPage; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index e08ba7b..166bff6 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -30,6 +30,7 @@ import { } from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; import dayjs from "dayjs"; +import { useTranslation, Trans } from "react-i18next"; import { eventiService } from "../services/eventiService"; import { demoService, DemoDataResult } from "../services/demoService"; import { StatoEvento } from "../types"; @@ -66,16 +67,16 @@ const StatCard = ({ ); -const getStatoLabel = (stato: StatoEvento) => { +const getStatoLabel = (stato: StatoEvento, t: any) => { switch (stato) { case StatoEvento.Scheda: - return "Scheda"; + return t("status.scheda"); case StatoEvento.Preventivo: - return "Preventivo"; + return t("status.preventivo"); case StatoEvento.Confermato: - return "Confermato"; + return t("status.confermato"); default: - return "Sconosciuto"; + return t("common.unknown"); } }; @@ -95,6 +96,7 @@ const getStatoColor = (stato: StatoEvento) => { export default function Dashboard() { const navigate = useNavigate(); const queryClient = useQueryClient(); + const { t } = useTranslation(); const [demoDialog, setDemoDialog] = useState<"generate" | "clear" | null>( null, ); @@ -116,7 +118,7 @@ export default function Dashboard() { queryClient.invalidateQueries(); } catch (err: any) { setError( - err.response?.data?.error || "Errore durante la generazione dei dati", + err.response?.data?.error || t("dashboard.generateError"), ); } finally { setLoading(false); @@ -132,7 +134,7 @@ export default function Dashboard() { queryClient.invalidateQueries(); } catch (err: any) { setError( - err.response?.data?.error || "Errore durante la pulizia dei dati", + err.response?.data?.error || t("dashboard.clearError"), ); } finally { setLoading(false); @@ -176,7 +178,7 @@ export default function Dashboard() { mb: 3, }} > - Dashboard + {t("dashboard.title")} @@ -200,7 +202,7 @@ export default function Dashboard() { } color="#1976d2" @@ -208,7 +210,7 @@ export default function Dashboard() { } color="#4caf50" @@ -216,7 +218,7 @@ export default function Dashboard() { } color="#ff9800" @@ -224,7 +226,7 @@ export default function Dashboard() { } color="#9c27b0" @@ -236,7 +238,7 @@ export default function Dashboard() { - Prossimi Eventi (30 giorni) + {t("dashboard.upcomingEvents")} {eventiProssimi.slice(0, 10).map((evento) => ( @@ -265,7 +267,7 @@ export default function Dashboard() { } /> @@ -273,7 +275,7 @@ export default function Dashboard() { ))} {eventiProssimi.length === 0 && ( - + )} @@ -283,7 +285,7 @@ export default function Dashboard() { - Preventivi in Scadenza + {t("dashboard.expiringQuotes")} {eventi @@ -310,10 +312,10 @@ export default function Dashboard() { > @@ -321,10 +323,10 @@ export default function Dashboard() { ))} {eventi.filter((e) => e.stato === StatoEvento.Preventivo) .length === 0 && ( - - - - )} + + + + )} @@ -332,18 +334,11 @@ export default function Dashboard() { {/* Dialog Genera Dati Demo */} - Genera Dati Demo + {t("dashboard.generateDialogTitle")} {!result && !error && ( - Questa operazione genera dati di test per dimostrazioni: -
- 15 Clienti -
- 10 Location -
- 12 Risorse (staff) -
- 20 Articoli -
- 20 Eventi con dettagli -
-
I dati esistenti non verranno modificati. + }} />
)} {loading && ( @@ -364,7 +359,7 @@ export default function Dashboard() {
{!result && ( )} @@ -380,24 +375,16 @@ export default function Dashboard() { {/* Dialog Pulisci Database */} - Pulisci Database + {t("dashboard.clearDialogTitle")} {!result && !error && ( - Attenzione: questa operazione elimina TUTTI i dati dal database! + {t("dashboard.clearDialogWarning")} )} {!result && !error && ( - Verranno eliminati: -
- Tutti gli eventi e i relativi dettagli -
- Tutti i clienti -
- Tutte le location -
- Tutte le risorse -
- Tutti gli articoli -
-
- Questa operazione non puo essere annullata. + }} />
)} {loading && ( @@ -407,9 +394,13 @@ export default function Dashboard() { )} {result && ( - Database pulito. Eliminati: {result.eventiCreati} eventi,{" "} - {result.clientiCreati} clienti, {result.locationCreate} location,{" "} - {result.risorseCreate} risorse, {result.articoliCreati} articoli. + {t("dashboard.clearSuccess", { + events: result.eventiCreati, + clients: result.clientiCreati, + locations: result.locationCreate, + resources: result.risorseCreate, + articles: result.articoliCreati + })} )} {error && ( @@ -420,7 +411,7 @@ export default function Dashboard() {
{!result && ( )} diff --git a/frontend/src/pages/EventiPage.tsx b/frontend/src/pages/EventiPage.tsx index 4fd7032..2d34f5d 100644 --- a/frontend/src/pages/EventiPage.tsx +++ b/frontend/src/pages/EventiPage.tsx @@ -28,16 +28,17 @@ import { Visibility as ViewIcon, } from '@mui/icons-material'; import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; import { eventiService } from '../services/eventiService'; import { lookupService } from '../services/lookupService'; import { Evento, StatoEvento } from '../types'; -const getStatoLabel = (stato: StatoEvento) => { +const getStatoLabel = (stato: StatoEvento, t: any) => { switch (stato) { - case StatoEvento.Scheda: return 'Scheda'; - case StatoEvento.Preventivo: return 'Preventivo'; - case StatoEvento.Confermato: return 'Confermato'; - default: return 'Sconosciuto'; + case StatoEvento.Scheda: return t('status.scheda'); + case StatoEvento.Preventivo: return t('status.preventivo'); + case StatoEvento.Confermato: return t('status.confermato'); + default: return t('common.unknown'); } }; @@ -53,6 +54,7 @@ const getStatoColor = (stato: StatoEvento): 'default' | 'warning' | 'success' => export default function EventiPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); + const { t } = useTranslation(); const [openDialog, setOpenDialog] = useState(false); const [formData, setFormData] = useState>({ dataEvento: dayjs().format('YYYY-MM-DD'), @@ -103,39 +105,39 @@ export default function EventiPage() { }); const columns: GridColDef[] = [ - { field: 'codice', headerName: 'Codice', width: 120 }, + { field: 'codice', headerName: t('events.code'), width: 120 }, { field: 'dataEvento', - headerName: 'Data', + headerName: t('events.date'), width: 120, valueFormatter: (value: string) => dayjs(value).format('DD/MM/YYYY'), }, - { field: 'descrizione', headerName: 'Descrizione', flex: 1, minWidth: 200 }, + { field: 'descrizione', headerName: t('events.description'), flex: 1, minWidth: 200 }, { field: 'cliente', - headerName: 'Cliente', + headerName: t('events.client'), width: 180, valueGetter: (value: any) => value?.ragioneSociale || '', }, { field: 'location', - headerName: 'Location', + headerName: t('events.location'), width: 150, valueGetter: (value: any) => value?.nome || '', }, { field: 'numeroOspiti', - headerName: 'Ospiti', + headerName: t('events.guests'), width: 80, align: 'center', }, { field: 'stato', - headerName: 'Stato', + headerName: t('events.status'), width: 120, renderCell: (params) => ( @@ -143,7 +145,7 @@ export default function EventiPage() { }, { field: 'actions', - headerName: 'Azioni', + headerName: t('common.actions'), width: 180, sortable: false, renderCell: (params) => ( @@ -161,7 +163,7 @@ export default function EventiPage() { size="small" color="error" onClick={() => { - if (confirm('Eliminare questo evento?')) { + if (confirm(t('common.deleteConfirm'))) { deleteMutation.mutate(params.row.id); } }} @@ -180,9 +182,9 @@ export default function EventiPage() { return ( - Eventi + {t('events.title')} @@ -201,26 +203,26 @@ export default function EventiPage() {
setOpenDialog(false)} maxWidth="sm" fullWidth> - Nuovo Evento + {t('events.newEvent')} setFormData({ ...formData, dataEvento: date?.format('YYYY-MM-DD') })} slotProps={{ textField: { fullWidth: true } }} /> setFormData({ ...formData, descrizione: e.target.value })} /> - Cliente + {t('events.client')} - Location + {t('events.location')} - Tipo Evento + {t('events.type')} - - + + diff --git a/frontend/src/pages/EventoDetailPage.tsx b/frontend/src/pages/EventoDetailPage.tsx index 070139f..bf26463 100644 --- a/frontend/src/pages/EventoDetailPage.tsx +++ b/frontend/src/pages/EventoDetailPage.tsx @@ -40,6 +40,7 @@ import { CheckCircle as ConfirmIcon, Print as PrintIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; import { eventiService } from "../services/eventiService"; import { lookupService } from "../services/lookupService"; @@ -67,20 +68,21 @@ function TabPanel(props: TabPanelProps) { ); } -const getStatoInfo = (stato: StatoEvento) => { +const getStatoInfo = (stato: StatoEvento, t: any) => { switch (stato) { case StatoEvento.Scheda: - return { label: "Scheda Evento", color: "#CAE3FC", textColor: "#1976d2" }; + return { label: t("events.detail.status.draft"), color: "#CAE3FC", textColor: "#1976d2" }; case StatoEvento.Preventivo: - return { label: "Preventivo", color: "#ffffb8", textColor: "#ed6c02" }; + return { label: t("events.detail.status.quote"), color: "#ffffb8", textColor: "#ed6c02" }; case StatoEvento.Confermato: - return { label: "Confermato", color: "#b8ffb8", textColor: "#2e7d32" }; + return { label: t("events.detail.status.confirmed"), color: "#b8ffb8", textColor: "#2e7d32" }; default: - return { label: "Nuovo", color: "#fafafa", textColor: "#666" }; + return { label: t("events.detail.status.new"), color: "#fafafa", textColor: "#666" }; } }; export default function EventoDetailPage() { + const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const location = useLocation(); @@ -323,11 +325,11 @@ export default function EventoDetailPage() { }); if (isLoading && !isNew) { - return Caricamento...; + return {t("events.detail.loading")}; } const data = isNew ? formData : { ...evento, ...formData }; - const statoInfo = getStatoInfo(data.stato || StatoEvento.Scheda); + const statoInfo = getStatoInfo(data.stato || StatoEvento.Scheda, t); const handleFieldChange = (field: string, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); @@ -373,8 +375,8 @@ export default function EventoDetailPage() { {statoInfo.label} - {data.codice || "Nuovo Evento"} -{" "} - {data.descrizione || "Senza descrizione"} + {data.codice || t("events.detail.newEvent")} -{" "} + {data.descrizione || t("events.detail.noDescription")} @@ -386,7 +388,7 @@ export default function EventoDetailPage() { onClick={() => duplicaMutation.mutate()} size="small" > - Duplica + {t("events.detail.actions.duplicate")} {data.stato !== StatoEvento.Confermato && ( )} @@ -417,7 +419,7 @@ export default function EventoDetailPage() { onClick={handleSave} disabled={!hasChanges && !isNew} > - Salva + {t("events.detail.actions.save")} {!isNew && ( )} @@ -443,7 +445,7 @@ export default function EventoDetailPage() { {/* Prima riga: Data, Orari, Tipo */} handleFieldChange("dataEvento", date?.format("YYYY-MM-DD")) @@ -455,7 +457,7 @@ export default function EventoDetailPage() { handleFieldChange("oraFine", time?.format("HH:mm:ss")) @@ -477,10 +479,10 @@ export default function EventoDetailPage() { - Tipo Evento + {t("events.detail.fields.type")} { if (!isNew) { cambiaStatoMutation.mutate(e.target.value as StatoEvento); @@ -628,9 +630,9 @@ export default function EventoDetailPage() { } }} > - Scheda - Preventivo - Confermato + {t("events.detail.status.draft")} + {t("events.detail.status.quote")} + {t("events.detail.status.confirmed")} @@ -645,13 +647,13 @@ export default function EventoDetailPage() { onChange={(_, v) => setTabValue(v)} sx={{ borderBottom: 1, borderColor: "divider" }} > - + - - - + + + {/* Tab Ospiti */} @@ -660,7 +662,7 @@ export default function EventoDetailPage() { sx={{ display: "flex", justifyContent: "space-between", mb: 2 }} > - Totale ospiti: {totaleOspiti} + {t("events.detail.guestsTab.total")}: {totaleOspiti} @@ -679,13 +681,13 @@ export default function EventoDetailPage() { - Tipo Ospite + {t("events.detail.guestsTab.type")} - Quantità + {t("events.detail.guestsTab.quantity")} - Note + {t("events.detail.guestsTab.notes")} @@ -711,17 +713,16 @@ export default function EventoDetailPage() { ))} {(!evento?.dettagliOspiti || evento.dettagliOspiti.length === 0) && ( - - - Nessun ospite aggiunto. Clicca "Aggiungi Tipo Ospite" - per iniziare. - - - )} + + + {t("events.detail.guestsTab.empty")} + + + )} @@ -733,7 +734,7 @@ export default function EventoDetailPage() { sx={{ display: "flex", justifyContent: "space-between", mb: 2 }} > - Articoli in lista:{" "} + {t("events.detail.withdrawalTab.total")}:{" "} {evento?.dettagliPrelievo?.length || 0} @@ -753,22 +754,22 @@ export default function EventoDetailPage() { - Codice + {t("events.detail.withdrawalTab.code")} - Articolo + {t("events.detail.withdrawalTab.article")} - Qta Richiesta + {t("events.detail.withdrawalTab.qtyRequested")} - Qta Calcolata + {t("events.detail.withdrawalTab.qtyCalculated")} - Qta Effettiva + {t("events.detail.withdrawalTab.qtyActual")} - Note + {t("events.detail.withdrawalTab.notes")} @@ -807,17 +808,16 @@ export default function EventoDetailPage() { ))} {(!evento?.dettagliPrelievo || evento.dettagliPrelievo.length === 0) && ( - - - Nessun articolo in lista. Clicca "Aggiungi Articolo" per - iniziare. - - - )} + + + {t("events.detail.withdrawalTab.empty")} + + + )} @@ -829,7 +829,7 @@ export default function EventoDetailPage() { sx={{ display: "flex", justifyContent: "space-between", mb: 2 }} > - Risorse assegnate:{" "} + {t("events.detail.resourcesTab.total")}:{" "} {evento?.dettagliRisorse?.length || 0} @@ -849,19 +849,19 @@ export default function EventoDetailPage() { - Risorsa + {t("events.detail.resourcesTab.resource")} - Ruolo + {t("common.role")} - Ora Inizio + {t("events.detail.fields.startTime")} - Ora Fine + {t("events.detail.fields.endTime")} - Note + {t("events.detail.resourcesTab.notes")} @@ -891,17 +891,16 @@ export default function EventoDetailPage() { ))} {(!evento?.dettagliRisorse || evento.dettagliRisorse.length === 0) && ( - - - Nessuna risorsa assegnata. Clicca "Aggiungi Risorsa" per - iniziare. - - - )} + + + {t("events.detail.resourcesTab.empty")} + + + )} @@ -979,14 +978,14 @@ export default function EventoDetailPage() { maxWidth="xs" fullWidth > - Aggiungi Tipo Ospite + {t("events.detail.dialogs.addGuest")} - Tipo Ospite + {t("events.detail.guestsTab.type")} - + @@ -1040,7 +1039,7 @@ export default function EventoDetailPage() { maxWidth="sm" fullWidth > - Aggiungi Articolo alla Lista + {t("events.detail.dialogs.addArticle")} ( - + )} /> - + @@ -1097,7 +1096,7 @@ export default function EventoDetailPage() { maxWidth="sm" fullWidth > - Aggiungi Risorsa + {t("events.detail.dialogs.addResource")} ( - + )} /> @@ -1124,7 +1123,7 @@ export default function EventoDetailPage() { - + diff --git a/frontend/src/pages/LocationPage.tsx b/frontend/src/pages/LocationPage.tsx index e7d15d3..c3a19ed 100644 --- a/frontend/src/pages/LocationPage.tsx +++ b/frontend/src/pages/LocationPage.tsx @@ -15,11 +15,13 @@ import { } from '@mui/material'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import { locationService } from '../services/lookupService'; import { Location } from '../types'; export default function LocationPage() { const queryClient = useQueryClient(); + const { t } = useTranslation(); const [openDialog, setOpenDialog] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState>({ attivo: true }); @@ -71,15 +73,15 @@ export default function LocationPage() { }; const columns: GridColDef[] = [ - { field: 'nome', headerName: 'Nome', flex: 1, minWidth: 200 }, - { field: 'citta', headerName: 'Città', width: 150 }, - { field: 'provincia', headerName: 'Prov.', width: 80 }, - { field: 'distanzaKm', headerName: 'Distanza (km)', width: 120, type: 'number' }, - { field: 'referente', headerName: 'Referente', width: 150 }, - { field: 'telefono', headerName: 'Telefono', width: 130 }, + { field: 'nome', headerName: t('location.name'), flex: 1, minWidth: 200 }, + { field: 'citta', headerName: t('location.city'), width: 150 }, + { field: 'provincia', headerName: t('location.province'), width: 80 }, + { field: 'distanzaKm', headerName: t('location.distance'), width: 120, type: 'number' }, + { field: 'referente', headerName: t('location.contact'), width: 150 }, + { field: 'telefono', headerName: t('location.phone'), width: 130 }, { field: 'actions', - headerName: 'Azioni', + headerName: t('common.actions'), width: 120, sortable: false, renderCell: (params) => ( @@ -91,7 +93,7 @@ export default function LocationPage() { size="small" color="error" onClick={() => { - if (confirm('Eliminare questa location?')) { + if (confirm(t('location.deleteConfirm'))) { deleteMutation.mutate(params.row.id); } }} @@ -106,9 +108,9 @@ export default function LocationPage() { return ( - Location + {t('location.title')} @@ -126,12 +128,12 @@ export default function LocationPage() {
- {editingId ? 'Modifica Location' : 'Nuova Location'} + {editingId ? t('location.editLocation') : t('location.newLocation')} setFormData({ ...formData, indirizzo: e.target.value })} @@ -148,7 +150,7 @@ export default function LocationPage() { setFormData({ ...formData, cap: e.target.value })} @@ -156,7 +158,7 @@ export default function LocationPage() { setFormData({ ...formData, citta: e.target.value })} @@ -164,7 +166,7 @@ export default function LocationPage() { setFormData({ ...formData, provincia: e.target.value })} @@ -172,7 +174,7 @@ export default function LocationPage() { setFormData({ ...formData, telefono: e.target.value })} @@ -189,7 +191,7 @@ export default function LocationPage() { setFormData({ ...formData, referente: e.target.value })} @@ -206,7 +208,7 @@ export default function LocationPage() { - + diff --git a/frontend/src/pages/ModulePurchasePage.tsx b/frontend/src/pages/ModulePurchasePage.tsx index 6dd2257..1862693 100644 --- a/frontend/src/pages/ModulePurchasePage.tsx +++ b/frontend/src/pages/ModulePurchasePage.tsx @@ -27,6 +27,7 @@ import { CalendarToday as AnnualIcon, Warning as WarningIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { useModule, useModules } from "../contexts/ModuleContext"; import { SubscriptionType, @@ -40,6 +41,7 @@ export default function ModulePurchasePage() { const location = useLocation(); const module = useModule(code || ""); const { enableModule, isModuleEnabled } = useModules(); + const { t } = useTranslation(); const [subscriptionType, setSubscriptionType] = useState( SubscriptionType.Annual, @@ -72,17 +74,17 @@ export default function ModulePurchasePage() { return ( - Modulo non trovato + {t("modules.admin.moduleNotFound")} - Il modulo richiesto non esiste. + {t("modules.admin.moduleNotFoundText")} ); @@ -95,7 +97,9 @@ export default function ModulePurchasePage() { : module.basePrice; const priceLabel = - subscriptionType === SubscriptionType.Monthly ? "/mese" : "/anno"; + subscriptionType === SubscriptionType.Monthly + ? t("modules.admin.perMonth") + : t("modules.admin.perYear"); // Calcola risparmio annuale const annualSavings = module.monthlyPrice * 12 - module.basePrice; @@ -117,13 +121,13 @@ export default function ModulePurchasePage() { onClick={() => navigate(-1)} sx={{ mb: 2 }} > - Indietro + {t("common.back")} - Attiva Modulo + {t("modules.admin.purchaseTitle")} - Scegli il piano di abbonamento per il modulo {module.name} + {t("modules.admin.purchaseSubtitle", { name: module.name })} @@ -131,7 +135,7 @@ export default function ModulePurchasePage() { {missingDependencies.length > 0 && ( }> - Questo modulo richiede i seguenti moduli che non sono attivi: + {t("modules.admin.missingDependencies")} {missingDependencies.map((dep) => ( @@ -165,7 +169,7 @@ export default function ModulePurchasePage() { {/* Selezione tipo abbonamento */} - Tipo di abbonamento + {t("modules.admin.subscriptionType")} - Mensile + {t("modules.admin.monthly")} {formatPrice(module.monthlyPrice)} @@ -187,7 +191,7 @@ export default function ModulePurchasePage() { variant="body2" color="text.secondary" > - /mese + {t("modules.admin.perMonth")} @@ -196,7 +200,7 @@ export default function ModulePurchasePage() { - Annuale + {t("modules.admin.annual")} {formatPrice(module.basePrice)} @@ -205,12 +209,14 @@ export default function ModulePurchasePage() { variant="body2" color="text.secondary" > - /anno + {t("modules.admin.perYear")} {savingsPercent > 0 && ( : null} - Rinnovo automatico alla scadenza + {t("modules.admin.autoRenewLabel")} @@ -244,26 +250,26 @@ export default function ModulePurchasePage() { sx={{ p: 2, mb: 3, bgcolor: "action.hover" }} > - Riepilogo ordine + {t("modules.admin.orderSummary")} - Modulo {module.name} + {t("modules.admin.module")} {module.name} {formatPrice(price)} - Abbonamento{" "} + {t("modules.admin.subscription")}{" "} {getSubscriptionTypeName(subscriptionType).toLowerCase()} {priceLabel} - Totale + {t("modules.admin.total")} {formatPrice(price)} {priceLabel} @@ -275,7 +281,7 @@ export default function ModulePurchasePage() { {enableMutation.isError && ( {(enableMutation.error as Error)?.message || - "Errore durante l'attivazione del modulo"} + t("modules.admin.activationError")} )} @@ -297,8 +303,8 @@ export default function ModulePurchasePage() { } > {enableMutation.isPending - ? "Attivazione in corso..." - : "Attiva Modulo"} + ? t("modules.admin.activating") + : t("modules.admin.activateModule")} {/* Note */} @@ -309,8 +315,7 @@ export default function ModulePurchasePage() { textAlign="center" sx={{ mt: 2 }} > - Potrai disattivare il modulo in qualsiasi momento dalle - impostazioni. I dati inseriti rimarranno disponibili. + {t("modules.admin.purchaseNote")} @@ -319,10 +324,10 @@ export default function ModulePurchasePage() { - Funzionalità incluse + {t("modules.admin.includedFeatures")} - {getModuleFeatures(module.code).map((feature, index) => ( + {getModuleFeatures(module.code, t).map((feature, index) => ( @@ -338,49 +343,10 @@ export default function ModulePurchasePage() { } // Helper per ottenere le funzionalità di un modulo -function getModuleFeatures(code: string): string[] { - const features: Record = { - warehouse: [ - "Gestione anagrafica articoli", - "Movimenti di magazzino (carico/scarico)", - "Giacenze in tempo reale", - "Valorizzazione scorte (FIFO, LIFO, medio ponderato)", - "Inventario e rettifiche", - "Report giacenze e movimenti", - ], - purchases: [ - "Gestione ordini a fornitore", - "DDT di entrata", - "Fatture passive", - "Scadenziario pagamenti", - "Analisi acquisti per fornitore/articolo", - "Storico prezzi di acquisto", - ], - sales: [ - "Gestione ordini cliente", - "DDT di uscita", - "Fatturazione elettronica", - "Scadenziario incassi", - "Analisi vendite per cliente/articolo", - "Listini prezzi", - ], - production: [ - "Distinte base multilivello", - "Cicli di lavoro", - "Ordini di produzione", - "Pianificazione MRP", - "Avanzamento produzione", - "Costi di produzione", - ], - quality: [ - "Piani di controllo", - "Registrazione controlli", - "Gestione non conformità", - "Azioni correttive/preventive", - "Certificazioni e audit", - "Statistiche qualità", - ], - }; - - return features[code] || ["Funzionalità complete del modulo"]; +function getModuleFeatures(code: string, t: (key: string) => string): string[] { + const featureKeys = [0, 1, 2, 3, 4, 5]; + if (["warehouse", "purchases", "sales", "production", "quality"].includes(code)) { + return featureKeys.map((i) => t(`modules.features.${code}.${i}`)); + } + return [t("modules.features.default")]; } diff --git a/frontend/src/pages/ModulesAdminPage.tsx b/frontend/src/pages/ModulesAdminPage.tsx index c5cf8b6..039c8b9 100644 --- a/frontend/src/pages/ModulesAdminPage.tsx +++ b/frontend/src/pages/ModulesAdminPage.tsx @@ -30,6 +30,7 @@ import { Autorenew as RenewIcon, } from "@mui/icons-material"; import * as Icons from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { useModules } from "../contexts/ModuleContext"; import { moduleService } from "../services/moduleService"; import type { ModuleDto } from "../types/module"; @@ -43,6 +44,7 @@ import { export default function ModulesAdminPage() { const navigate = useNavigate(); + const { t } = useTranslation(); const { modules, isLoading, refreshModules } = useModules(); const [selectedModule, setSelectedModule] = useState(null); const [confirmDisable, setConfirmDisable] = useState(null); @@ -108,10 +110,10 @@ export default function ModulesAdminPage() { > - Gestione Moduli + {t("modules.admin.title")} - Configura i moduli attivi e gestisci le subscription + {t("modules.admin.subtitle")} @@ -121,14 +123,14 @@ export default function ModulesAdminPage() { onClick={() => checkExpiredMutation.mutate()} disabled={checkExpiredMutation.isPending} > - Controlla Scadenze + {t("modules.admin.checkExpired")} @@ -137,8 +139,9 @@ export default function ModulesAdminPage() { {expiringModules.length > 0 && ( }> - {expiringModules.length} modulo/i in scadenza nei prossimi 30 - giorni: + {t("modules.admin.expiringWarning", { + count: expiringModules.length, + })} {expiringModules.map((m) => ( @@ -170,6 +173,7 @@ export default function ModulesAdminPage() { onRenew={() => renewMutation.mutate(module.code)} isRenewing={renewMutation.isPending} getIcon={getModuleIcon} + t={t} />
))} @@ -198,7 +202,7 @@ export default function ModulesAdminPage() { - Prezzo annuale + {t("modules.admin.annualPrice")} {formatPrice(selectedModule.basePrice)} @@ -206,7 +210,7 @@ export default function ModulesAdminPage() { - Prezzo mensile + {t("modules.admin.monthlyPrice")} {formatPrice(selectedModule.monthlyPrice)} @@ -217,7 +221,7 @@ export default function ModulesAdminPage() { {selectedModule.dependencies.length > 0 && ( - Dipendenze + {t("modules.admin.dependencies")} {selectedModule.dependencies.map((dep) => ( @@ -231,12 +235,12 @@ export default function ModulesAdminPage() { <> - Dettagli Subscription + {t("modules.admin.subscriptionDetails")} - Tipo + {t("modules.admin.type")} {selectedModule.subscription.subscriptionTypeName} @@ -244,7 +248,7 @@ export default function ModulesAdminPage() { - Stato + {t("modules.admin.status")} - Data inizio + {t("modules.admin.startDate")} {formatDate(selectedModule.subscription.startDate)} @@ -268,7 +272,7 @@ export default function ModulesAdminPage() { - Data scadenza + {t("modules.admin.endDate")} {formatDate(selectedModule.subscription.endDate)} @@ -276,7 +280,7 @@ export default function ModulesAdminPage() { - Giorni rimanenti + {t("modules.admin.daysRemaining")} {getDaysRemainingText( @@ -286,10 +290,12 @@ export default function ModulesAdminPage() { - Rinnovo automatico + {t("modules.admin.autoRenew")} - {selectedModule.subscription.autoRenew ? "Sì" : "No"} + {selectedModule.subscription.autoRenew + ? t("modules.admin.yes") + : t("modules.admin.no")} @@ -297,7 +303,9 @@ export default function ModulesAdminPage() { )} - + )} @@ -310,28 +318,29 @@ export default function ModulesAdminPage() { maxWidth="xs" fullWidth > - Conferma disattivazione + {t("modules.admin.disableConfirmTitle")} - Sei sicuro di voler disattivare il modulo{" "} + {t("modules.admin.disableConfirmText")}{" "} {modules.find((m) => m.code === confirmDisable)?.name} ? - I dati inseriti rimarranno nel sistema ma non saranno più - accessibili fino alla riattivazione. + {t("modules.admin.disableConfirmSubtext")} {disableMutation.isError && ( {(disableMutation.error as Error)?.message || - "Errore durante la disattivazione"} + t("common.error")} )} - + @@ -361,6 +370,7 @@ interface ModuleCardProps { onRenew: () => void; isRenewing: boolean; getIcon: (iconName?: string) => React.ReactNode; + t: (key: string) => string; } function ModuleCard({ @@ -370,6 +380,7 @@ function ModuleCard({ onRenew, isRenewing, getIcon, + t, }: ModuleCardProps) { const statusColor = getSubscriptionStatusColor(module.subscription); const statusText = getSubscriptionStatusText(module.subscription); @@ -409,7 +420,7 @@ function ModuleCard({ {module.isCore ? ( - + ) : ( )} @@ -435,7 +446,7 @@ function ModuleCard({ {module.subscription && module.isEnabled && ( - Scadenza: {formatDate(module.subscription.endDate)} + {t("modules.admin.endDate")}: {formatDate(module.subscription.endDate)} {module.subscription.daysRemaining !== undefined && module.subscription.daysRemaining <= 30 && ( - + @@ -483,7 +494,7 @@ function ModuleCard({ {module.isEnabled && !module.isCore && module.subscription?.isExpiringSoon && ( - + } - label={module.isEnabled ? "Attivo" : "Disattivo"} + label={module.isEnabled ? t("modules.admin.active") : t("modules.admin.inactive")} labelPlacement="start" /> )} diff --git a/frontend/src/pages/ReportEditorPage.tsx b/frontend/src/pages/ReportEditorPage.tsx index a5bf6ee..c3e458c 100644 --- a/frontend/src/pages/ReportEditorPage.tsx +++ b/frontend/src/pages/ReportEditorPage.tsx @@ -46,6 +46,7 @@ import { Close as CloseIcon, Layers as LayersIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import EditorCanvas, { type ContextMenuEvent, type EditorCanvasRef, @@ -101,6 +102,7 @@ export default function ReportEditorPage() { const queryClient = useQueryClient(); const isNew = !id; const theme = useTheme(); + const { t } = useTranslation(); // Responsive breakpoints const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px @@ -118,7 +120,7 @@ export default function ReportEditorPage() { descrizione: string; categoria: string; }>({ - nome: "Nuovo Template", + nome: t("reports.editor.newTemplate"), descrizione: "", categoria: "Generale", }); @@ -370,7 +372,7 @@ export default function ReportEditorPage() { // Show notification setSnackbar({ open: true, - message: "Template aggiornato da un altro utente", + message: t("reports.editor.templateUpdatedByOther"), severity: "success", }); @@ -596,7 +598,7 @@ export default function ReportEditorPage() { queryClient.invalidateQueries({ queryKey: ["report-templates"] }); setSnackbar({ open: true, - message: "Template salvato con successo", + message: t("reports.editor.saveSuccess"), severity: "success", }); setSaveDialog(false); @@ -636,7 +638,7 @@ export default function ReportEditorPage() { onError: (error) => { setSnackbar({ open: true, - message: `Errore nel salvataggio: ${error}`, + message: t("reports.editor.saveError", { error }), severity: "error", }); }, @@ -693,7 +695,7 @@ export default function ReportEditorPage() { const newPageId = `page-${uuidv4().slice(0, 8)}`; const newPage: AprtPage = { id: newPageId, - name: `Pagina ${template.pages.length + 1}`, + name: t("reports.editor.pageName", { number: template.pages.length + 1 }), }; historyActions.set((prev) => ({ @@ -721,7 +723,7 @@ export default function ReportEditorPage() { const newPage: AprtPage = { ...sourcePage, id: newPageId, - name: `${sourcePage.name} (copia)`, + name: t("reports.editor.copyOf", { name: sourcePage.name }), }; // Duplicate elements from source page @@ -871,7 +873,7 @@ export default function ReportEditorPage() { style: { ...defaultStyle }, content: type === "text" - ? { type: "static", value: "Nuovo testo" } + ? { type: "static", value: t("reports.editor.newText") } : undefined, visible: true, locked: false, @@ -879,19 +881,19 @@ export default function ReportEditorPage() { columns: type === "table" ? [ - { - field: "campo1", - header: "Colonna 1", - width: 50, - align: "left", - }, - { - field: "campo2", - header: "Colonna 2", - width: 50, - align: "left", - }, - ] + { + field: "campo1", + header: t("reports.editor.column", { number: 1 }), + width: 50, + align: "left", + }, + { + field: "campo2", + header: t("reports.editor.column", { number: 2 }), + width: 50, + align: "left", + }, + ] : undefined, }; @@ -1048,7 +1050,7 @@ export default function ReportEditorPage() { const copy: AprtElement = { ...selectedElement, id: uuidv4(), - name: `${selectedElement.name}_copia`, + name: `${selectedElement.name}${t("reports.editor.copySuffix")}`, position: { ...selectedElement.position, x: selectedElement.position.x + 10, @@ -1139,7 +1141,7 @@ export default function ReportEditorPage() { setClipboard({ ...selectedElement }); setSnackbar({ open: true, - message: "Elemento copiato", + message: t("reports.editor.elementCopied"), severity: "success", }); }, [selectedElement]); @@ -1150,7 +1152,7 @@ export default function ReportEditorPage() { const pastedElement: AprtElement = { ...clipboard, id: uuidv4(), - name: `${clipboard.name}_incollato`, + name: `${clipboard.name}${t("reports.editor.pastedSuffix")}`, position: { ...clipboard.position, x: clipboard.position.x + 10, @@ -1399,7 +1401,7 @@ export default function ReportEditorPage() { const handleGroup = useCallback(() => { setSnackbar({ open: true, - message: "Raggruppamento non ancora implementato", + message: t("reports.editor.groupingNotImplemented"), severity: "error", }); }, []); @@ -1407,7 +1409,7 @@ export default function ReportEditorPage() { const handleUngroup = useCallback(() => { setSnackbar({ open: true, - message: "Separazione non ancora implementata", + message: t("reports.editor.ungroupingNotImplemented"), severity: "error", }); }, []); @@ -1445,7 +1447,7 @@ export default function ReportEditorPage() { // For now, just select the element setSnackbar({ open: true, - message: "Fai doppio click sul testo per modificarlo", + message: t("reports.editor.doubleClickToEdit"), severity: "success", }); }, []); @@ -1462,7 +1464,7 @@ export default function ReportEditorPage() { // This would need to calculate text dimensions setSnackbar({ open: true, - message: "Adatta al contenuto non ancora implementato", + message: t("reports.editor.fitToContentNotImplemented"), severity: "error", }); }, []); @@ -1483,7 +1485,7 @@ export default function ReportEditorPage() { if (selectedDatasets.length === 0) { setSnackbar({ open: true, - message: "Seleziona almeno un dataset per l'anteprima", + message: t("reports.editor.selectDatasetForPreview"), severity: "error", }); return; @@ -1501,7 +1503,7 @@ export default function ReportEditorPage() { if (isNew) { setSnackbar({ open: true, - message: "Salva il template prima di visualizzare l'anteprima", + message: t("reports.editor.saveBeforePreview"), severity: "error", }); setPreviewDialog(false); @@ -1518,7 +1520,7 @@ export default function ReportEditorPage() { } catch (error) { setSnackbar({ open: true, - message: `Errore nella generazione dell'anteprima: ${error}`, + message: t("reports.editor.previewError", { error }), severity: "error", }); } finally { @@ -1847,11 +1849,11 @@ export default function ReportEditorPage() { const getMobilePanelTitle = () => { switch (mobilePanel) { case "pages": - return "Pagine"; + return t("reports.editor.panels.pages"); case "data": - return "Campi Dati"; + return t("reports.editor.panels.data"); case "properties": - return "Proprietà"; + return t("reports.editor.panels.properties"); default: return ""; } @@ -1902,7 +1904,7 @@ export default function ReportEditorPage() { isSaving={saveMutation.isPending} currentPageIndex={currentPageIndex} totalPages={template.pages.length} - currentPageName={currentPage?.name || "Pagina 1"} + currentPageName={currentPage?.name || t("reports.editor.defaultPageName")} onPrevPage={handlePrevPage} onNextPage={handleNextPage} hasUnsavedChanges={hasUnsavedChanges} @@ -1932,7 +1934,7 @@ export default function ReportEditorPage() { } position="left" flex={panelState.flex} @@ -1955,7 +1957,7 @@ export default function ReportEditorPage() { } position="left" flex={panelState.flex} @@ -1982,7 +1984,7 @@ export default function ReportEditorPage() { } position="left" flex={panelState.flex} @@ -2073,7 +2075,7 @@ export default function ReportEditorPage() { } position={panelState.position} flex={panelState.flex} @@ -2096,7 +2098,7 @@ export default function ReportEditorPage() { } position={panelState.position} flex={panelState.flex} @@ -2123,7 +2125,7 @@ export default function ReportEditorPage() { } position={panelState.position} flex={panelState.flex} @@ -2169,17 +2171,17 @@ export default function ReportEditorPage() { showLabels > } /> } /> } /> @@ -2192,7 +2194,7 @@ export default function ReportEditorPage() { anchor="bottom" open={isMobile && mobilePanel !== null} onClose={() => setMobilePanel(null)} - onOpen={() => {}} + onOpen={() => { }} disableSwipeToOpen PaperProps={{ sx: { @@ -2241,11 +2243,11 @@ export default function ReportEditorPage() { fullWidth fullScreen={isMobile} > - Salva Template + {t("reports.editor.saveDialog.title")} setTemplateInfo((prev) => ({ ...prev, nome: e.target.value })) @@ -2254,7 +2256,7 @@ export default function ReportEditorPage() { required /> setTemplateInfo((prev) => ({ @@ -2267,10 +2269,10 @@ export default function ReportEditorPage() { rows={2} /> - Categoria + {t("reports.editor.saveDialog.category")} diff --git a/frontend/src/pages/ReportTemplatesPage.tsx b/frontend/src/pages/ReportTemplatesPage.tsx index 62f1cd3..292a72b 100644 --- a/frontend/src/pages/ReportTemplatesPage.tsx +++ b/frontend/src/pages/ReportTemplatesPage.tsx @@ -37,6 +37,7 @@ import { Upload as UploadIcon, Description as DescriptionIcon, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import { reportTemplateService, downloadBlob } from "../services/reportService"; import type { ReportTemplateDto } from "../types/report"; @@ -44,6 +45,7 @@ export default function ReportTemplatesPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const theme = useTheme(); + const { t } = useTranslation(); // Breakpoints const isMobile = useMediaQuery(theme.breakpoints.down("sm")); @@ -161,7 +163,7 @@ export default function ReportTemplatesPage() { }} > - Template Report + {t("reports.title")} {/* Desktop/Tablet buttons */} @@ -172,14 +174,14 @@ export default function ReportTemplatesPage() { startIcon={} onClick={() => setImportDialog(true)} > - Importa + {t("reports.import")} )} @@ -196,16 +198,16 @@ export default function ReportTemplatesPage() { }} > - Filtra per categoria + {t("reports.filterCategory")} @@ -219,7 +221,7 @@ export default function ReportTemplatesPage() { onClick={() => setImportDialog(true)} fullWidth > - Importa Template + {t("reports.importTemplate")} )} @@ -242,10 +244,10 @@ export default function ReportTemplatesPage() { }} /> - Nessun template trovato + {t("reports.noTemplates")} - Crea il tuo primo template di report o importane uno esistente + {t("reports.createFirstTemplate")} setImportDialog(true)} fullWidth={isMobile} > - Importa Template + {t("reports.importTemplate")} @@ -342,7 +344,7 @@ export default function ReportTemplatesPage() { {template.nome} {template.pageSize} -{" "} {template.orientation === "portrait" - ? "Verticale" - : "Orizzontale"} + ? t("reports.vertical") + : t("reports.horizontal")} @@ -382,7 +384,7 @@ export default function ReportTemplatesPage() { }} > - + @@ -392,7 +394,7 @@ export default function ReportTemplatesPage() { - + cloneMutation.mutate(template.id)} @@ -400,7 +402,7 @@ export default function ReportTemplatesPage() { - + handleExport(template)} @@ -411,7 +413,7 @@ export default function ReportTemplatesPage() { - + navigate("/report-editor")} sx={{ position: "fixed", @@ -454,14 +456,13 @@ export default function ReportTemplatesPage() { maxWidth="xs" fullScreen={isMobile} > - Conferma Eliminazione + {t("reports.confirmDelete")} - Sei sicuro di voler eliminare il template " - {deleteDialog.template?.nome}"? + {t("reports.deleteConfirmText", { name: deleteDialog.template?.nome })} - Questa azione non può essere annullata. + {t("reports.irreversibleAction")} @@ -469,7 +470,7 @@ export default function ReportTemplatesPage() { onClick={() => setDeleteDialog({ open: false, template: null })} fullWidth={isMobile} > - Annulla + {t("reports.cancel")} @@ -497,13 +498,13 @@ export default function ReportTemplatesPage() { maxWidth="xs" fullScreen={isMobile} > - Importa Template + {t("reports.importTitle")} - Seleziona un file .aprt da importare + {t("reports.importText")} diff --git a/frontend/src/pages/RisorsePage.tsx b/frontend/src/pages/RisorsePage.tsx index 11c7875..54d9e59 100644 --- a/frontend/src/pages/RisorsePage.tsx +++ b/frontend/src/pages/RisorsePage.tsx @@ -19,11 +19,13 @@ import { } from '@mui/material'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; import { risorseService, lookupService } from '../services/lookupService'; import { Risorsa } from '../types'; export default function RisorsePage() { const queryClient = useQueryClient(); + const { t } = useTranslation(); const [openDialog, setOpenDialog] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState>({ attivo: true }); @@ -80,19 +82,19 @@ export default function RisorsePage() { }; const columns: GridColDef[] = [ - { field: 'nome', headerName: 'Nome', width: 150 }, - { field: 'cognome', headerName: 'Cognome', width: 150 }, + { field: 'nome', headerName: t('resources.name'), width: 150 }, + { field: 'cognome', headerName: t('resources.surname'), width: 150 }, { field: 'tipoRisorsa', - headerName: 'Tipo', + headerName: t('resources.type'), width: 150, valueGetter: (value: any) => value?.descrizione || '', }, - { field: 'telefono', headerName: 'Telefono', width: 130 }, - { field: 'email', headerName: 'Email', flex: 1, minWidth: 200 }, + { field: 'telefono', headerName: t('resources.phone'), width: 130 }, + { field: 'email', headerName: t('resources.email'), flex: 1, minWidth: 200 }, { field: 'actions', - headerName: 'Azioni', + headerName: t('common.actions'), width: 120, sortable: false, renderCell: (params) => ( @@ -104,7 +106,7 @@ export default function RisorsePage() { size="small" color="error" onClick={() => { - if (confirm('Eliminare questa risorsa?')) { + if (confirm(t('resources.deleteConfirm'))) { deleteMutation.mutate(params.row.id); } }} @@ -119,9 +121,9 @@ export default function RisorsePage() { return ( - Risorse + {t('resources.title')} @@ -139,12 +141,12 @@ export default function RisorsePage() { - {editingId ? 'Modifica Risorsa' : 'Nuova Risorsa'} + {editingId ? t('resources.editResource') : t('resources.newResource')} setFormData({ ...formData, cognome: e.target.value })} @@ -161,10 +163,10 @@ export default function RisorsePage() { - Tipo Risorsa + {t('resources.resourceType')}