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(
"categoryId",
@@ -408,7 +409,7 @@ export default function ArticleFormPage() {
}
>
- Nessuna
+ {t("warehouse.articleForm.options.noCategory")}
{flatCategories.map((cat) => (
@@ -421,7 +422,7 @@ export default function ArticleFormPage() {
handleChange("unitOfMeasure", e.target.value)
@@ -434,7 +435,7 @@ export default function ArticleFormPage() {
handleChange("barcode", e.target.value)}
/>
@@ -444,13 +445,13 @@ export default function ArticleFormPage() {
- Livelli di Scorta
+ {t("warehouse.articleForm.sections.stockLevels")}
@@ -465,7 +466,7 @@ export default function ArticleFormPage() {
@@ -480,7 +481,7 @@ export default function ArticleFormPage() {
@@ -495,7 +496,7 @@ export default function ArticleFormPage() {
@@ -512,13 +513,13 @@ export default function ArticleFormPage() {
- Costi e Valorizzazione
+ {t("warehouse.articleForm.sections.costs")}
@@ -537,18 +538,18 @@ export default function ArticleFormPage() {
- Gestione Stock
+ {t("warehouse.articleForm.fields.stockManagement")}
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")}
handleChange("valuationMethod", e.target.value)
}
>
{Object.entries(valuationMethodLabels).map(
- ([value, label]) => (
+ ([value]) => (
- {label}
+ {t(`warehouse.valuationMethod.${ValuationMethod[parseInt(value, 10)]}`)}
),
)}
@@ -580,7 +581,7 @@ export default function ArticleFormPage() {
- Tracciabilità
+ {t("warehouse.articleForm.sections.traceability")}
@@ -594,7 +595,7 @@ export default function ArticleFormPage() {
disabled={!isNew && article?.isBatchManaged}
/>
}
- label="Gestione Lotti"
+ label={t("warehouse.articleForm.fields.batchManaged")}
/>
@@ -608,7 +609,7 @@ export default function ArticleFormPage() {
disabled={!isNew && article?.isSerialManaged}
/>
}
- label="Gestione Matricole"
+ label={t("warehouse.articleForm.fields.serialManaged")}
/>
@@ -621,7 +622,7 @@ export default function ArticleFormPage() {
}
/>
}
- label="Gestione Scadenza"
+ label={t("warehouse.articleForm.fields.expiryManaged")}
/>
@@ -634,7 +635,7 @@ export default function ArticleFormPage() {
}
/>
}
- label="Articolo Attivo"
+ label={t("warehouse.articleForm.fields.active")}
/>
@@ -643,7 +644,7 @@ export default function ArticleFormPage() {
handleChange("notes", e.target.value)}
multiline
@@ -656,14 +657,14 @@ export default function ArticleFormPage() {
- Immagine
+ {t("warehouse.articleForm.sections.image")}
{imagePreview ? (
}
fullWidth
>
- Carica
+ {t("warehouse.articleForm.actions.upload")}
- 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 */}
- navigate(-1)}>Annulla
+ navigate(-1)}>{t("warehouse.articleForm.actions.cancel")}
- {isPending ? "Salvataggio..." : "Salva"}
+ {isPending ? t("warehouse.articleForm.actions.saving") : t("warehouse.articleForm.actions.save")}
@@ -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")}
}
onClick={nav.goToNewArticle}
>
- Nuovo Articolo
+ {t("warehouse.articles.newArticle")}
@@ -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")}
setCategoryId(e.target.value as number | "")}
>
- Tutte
+ {t("warehouse.articles.filters.all")}
{flatCategories.map((cat) => (
@@ -311,7 +313,7 @@ export default function ArticlesPage() {
onClick={() => setShowInactive(!showInactive)}
fullWidth
>
- {showInactive ? "Mostra Tutti" : "Solo Attivi"}
+ {showInactive ? t("warehouse.articles.filters.showAll") : t("warehouse.articles.filters.onlyActive")}
-
+
-
+
@@ -381,7 +383,7 @@ export default function ArticlesPage() {
sx={{ fontSize: 48, color: "grey.400", mb: 2 }}
/>
- Nessun articolo trovato
+ {t("warehouse.articles.noArticlesFound")}
@@ -445,7 +447,7 @@ export default function ArticlesPage() {
nav.goToEditArticle(article.id);
}}
>
- Modifica
+ {t("warehouse.articles.actions.edit")}
- Modifica
+ {t("warehouse.articles.actions.edit")}
- Visualizza Giacenze
+ {t("warehouse.articles.actions.viewStock")}
- Elimina
+ {t("warehouse.articles.actions.delete")}
@@ -493,28 +495,28 @@ export default function ArticlesPage() {
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
>
- Conferma Eliminazione
+ {t("warehouse.articles.deleteDialog.title")}
- Sei sicuro di voler eliminare l'articolo{" "}
-
- {articleToDelete?.code} - {articleToDelete?.description}
-
- ?
+ }}
+ />
- Questa azione non può essere annullata.
+ {t("warehouse.articles.deleteDialog.warning")}
- setDeleteDialogOpen(false)}>Annulla
+ setDeleteDialogOpen(false)}>{t("warehouse.articles.deleteDialog.cancel")}
- {deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
+ {deleteMutation.isPending ? t("warehouse.articles.deleteDialog.deleting") : t("warehouse.articles.deleteDialog.delete")}
diff --git a/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx b/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx
index 91272da..6e5b612 100644
--- a/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx
+++ b/frontend/src/modules/warehouse/pages/InboundMovementPage.tsx
@@ -32,6 +32,7 @@ import {
Delete as DeleteIcon,
Check as ConfirmIcon,
} 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";
@@ -54,6 +55,7 @@ interface MovementLine {
}
export default function InboundMovementPage() {
+ const { t } = useTranslation();
const navigate = useNavigate();
const [movementDate, setMovementDate] = useState(dayjs());
const [warehouseId, setWarehouseId] = useState("");
@@ -124,14 +126,14 @@ export default function InboundMovementPage() {
const validate = (): boolean => {
const newErrors: Record = {};
if (!warehouseId) {
- newErrors.warehouseId = "Seleziona un magazzino";
+ newErrors.warehouseId = t("warehouse.inbound.validation.warehouseRequired");
}
if (!movementDate) {
- newErrors.movementDate = "Inserisci la data";
+ newErrors.movementDate = t("warehouse.inbound.validation.dateRequired");
}
const validLines = lines.filter((l) => l.article && l.quantity > 0);
if (validLines.length === 0) {
- newErrors.lines = "Inserisci almeno una riga con articolo e quantità";
+ newErrors.lines = t("warehouse.inbound.validation.linesRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@@ -179,30 +181,29 @@ export default function InboundMovementPage() {
- Nuovo Carico
+ {t("warehouse.inbound.title")}
- Movimento di entrata merce in magazzino
+ {t("warehouse.inbound.subtitle")}
{(createMutation.error || confirmMutation.error) && (
- Errore:{" "}
- {((createMutation.error || confirmMutation.error) as Error).message}
+ {t("warehouse.inbound.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })}
)}
{/* Form Header */}
- Dati Movimento
+ {t("warehouse.inbound.sections.movementData")}
- Magazzino
+ {t("warehouse.inbound.fields.warehouse")}
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")}
} onClick={handleAddLine}>
- Aggiungi Riga
+ {t("warehouse.inbound.actions.addLine")}
@@ -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 */}
- navigate(-1)}>Annulla
+ navigate(-1)}>{t("warehouse.inbound.actions.cancel")}
handleSubmit(false)}
disabled={isPending}
>
- Salva Bozza
+ {t("warehouse.inbound.actions.saveDraft")}
- Salva e Conferma
+ {t("warehouse.inbound.actions.saveAndConfirm")}
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")}
- setConfirmDialogOpen(false)}>Annulla
+ setConfirmDialogOpen(false)}>{t("warehouse.inventory.count.confirmDialog.cancel")}
confirmMutation.mutate()}
color="success"
variant="contained"
autoFocus
>
- Conferma
+ {t("warehouse.inventory.count.confirmDialog.confirm")}
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")}
}
onClick={() => navigate("/warehouse/inventory")}
>
- Indietro
+ {t("warehouse.inventory.form.actions.back")}
@@ -139,7 +141,7 @@ export default function InventoryFormPage() {
- Magazzino
+ {t("warehouse.inventory.form.fields.warehouse")}
setFormData({
...formData,
@@ -185,10 +187,10 @@ export default function InventoryFormPage() {
- Categoria (Opzionale)
+ {t("warehouse.inventory.form.fields.category")}
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")}
setFormData({
...formData,
@@ -220,16 +222,16 @@ export default function InventoryFormPage() {
})
}
>
- Completo
- Parziale
- Ciclico
- A Campione
+ {t("warehouse.inventory.form.options.type.Full")}
+ {t("warehouse.inventory.form.options.type.Partial")}
+ {t("warehouse.inventory.form.options.type.Cyclic")}
+ {t("warehouse.inventory.form.options.type.Sample")}
}
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")}
}
onClick={handleCreate}
>
- Nuovo Inventario
+ {t("warehouse.inventory.newInventory")}
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")}
setWarehouseId(e.target.value as number | "")
}
>
- Tutti
+ {t("warehouse.movements.filters.all")}
{warehouses?.map((w) => (
@@ -398,20 +400,20 @@ export default function MovementsPage() {
- Tipo
+ {t("warehouse.movements.filters.type")}
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")}
setStatus(e.target.value as MovementStatus | "")
}
>
- Tutti
+ {t("warehouse.movements.filters.all")}
{Object.entries(movementStatusLabels).map(
- ([value, label]) => (
+ ([value]) => (
- {label}
+ {t(`warehouse.movementStatus.${MovementStatus[parseInt(value, 10)]}`)}
),
)}
@@ -442,7 +444,7 @@ export default function MovementsPage() {
- }
- onClick={clearFilters}
- >
- Reset
-
-
- )}
+
+ }
+ onClick={clearFilters}
+ >
+ {t("warehouse.movements.filters.reset")}
+
+
+ )}
@@ -498,7 +500,7 @@ export default function MovementsPage() {
{/* Speed Dial for New Movements */}
} />}
open={speedDialOpen}
@@ -529,7 +531,7 @@ export default function MovementsPage() {
- Visualizza
+ {t("warehouse.movements.actions.view")}
{menuMovement?.status === MovementStatus.Draft && (
<>
@@ -537,13 +539,13 @@ export default function MovementsPage() {
- Conferma
+ {t("warehouse.movements.actions.confirm")}
- Annulla
+ {t("warehouse.movements.actions.cancel")}
- Elimina
+ {t("warehouse.movements.actions.delete")}
>
)}
@@ -563,26 +565,28 @@ export default function MovementsPage() {
open={confirmDialogOpen}
onClose={() => setConfirmDialogOpen(false)}
>
- Conferma Movimento
+ {t("warehouse.movements.dialogs.confirm.title")}
- Confermare il movimento{" "}
- {menuMovement?.documentNumber} ?
+ }}
+ />
- Le giacenze verranno aggiornate e il movimento non potrà più
- essere modificato.
+ {t("warehouse.movements.dialogs.confirm.warning")}
- setConfirmDialogOpen(false)}>Annulla
+ setConfirmDialogOpen(false)}>{t("warehouse.movements.dialogs.confirm.cancel")}
- {confirmMutation.isPending ? "Conferma..." : "Conferma"}
+ {confirmMutation.isPending ? t("warehouse.movements.dialogs.confirm.confirming") : t("warehouse.movements.dialogs.confirm.confirm")}
@@ -592,18 +596,21 @@ export default function MovementsPage() {
open={cancelDialogOpen}
onClose={() => setCancelDialogOpen(false)}
>
- Annulla Movimento
+ {t("warehouse.movements.dialogs.cancel.title")}
- Annullare il movimento{" "}
- {menuMovement?.documentNumber} ?
+ }}
+ />
- Il movimento verrà marcato come annullato ma non eliminato.
+ {t("warehouse.movements.dialogs.cancel.warning")}
- setCancelDialogOpen(false)}>Indietro
+ setCancelDialogOpen(false)}>{t("warehouse.movements.dialogs.cancel.back")}
{cancelMutation.isPending
- ? "Annullamento..."
- : "Annulla Movimento"}
+ ? t("warehouse.movements.dialogs.cancel.cancelling")
+ : t("warehouse.movements.dialogs.cancel.cancelMovement")}
@@ -622,25 +629,28 @@ export default function MovementsPage() {
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
>
- Elimina Movimento
+ {t("warehouse.movements.dialogs.delete.title")}
- Eliminare definitivamente il movimento{" "}
- {menuMovement?.documentNumber} ?
+ }}
+ />
- Questa azione non può essere annullata.
+ {t("warehouse.movements.dialogs.delete.warning")}
- setDeleteDialogOpen(false)}>Annulla
+ setDeleteDialogOpen(false)}>{t("warehouse.movements.dialogs.delete.cancel")}
- {deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
+ {deleteMutation.isPending ? t("warehouse.movements.dialogs.delete.deleting") : t("warehouse.movements.dialogs.delete.delete")}
diff --git a/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx b/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx
index b39de61..5fb3f54 100644
--- a/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx
+++ b/frontend/src/modules/warehouse/pages/OutboundMovementPage.tsx
@@ -34,6 +34,7 @@ import {
Check as ConfirmIcon,
Warning as WarningIcon,
} 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";
@@ -57,6 +58,7 @@ interface MovementLine {
}
export default function OutboundMovementPage() {
+ const { t } = useTranslation();
const navigate = useNavigate();
const [movementDate, setMovementDate] = useState(dayjs());
const [warehouseId, setWarehouseId] = useState("");
@@ -138,14 +140,14 @@ export default function OutboundMovementPage() {
const validate = (): boolean => {
const newErrors: Record = {};
if (!warehouseId) {
- newErrors.warehouseId = "Seleziona un magazzino";
+ newErrors.warehouseId = t("warehouse.outbound.validation.warehouseRequired");
}
if (!movementDate) {
- newErrors.movementDate = "Inserisci la data";
+ newErrors.movementDate = t("warehouse.outbound.validation.dateRequired");
}
const validLines = lines.filter((l) => l.article && l.quantity > 0);
if (validLines.length === 0) {
- newErrors.lines = "Inserisci almeno una riga con articolo e quantità";
+ newErrors.lines = t("warehouse.outbound.validation.linesRequired");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
@@ -192,36 +194,35 @@ export default function OutboundMovementPage() {
- Nuovo Scarico
+ {t("warehouse.outbound.title")}
- Movimento di uscita merce da magazzino
+ {t("warehouse.outbound.subtitle")}
{(createMutation.error || confirmMutation.error) && (
- Errore:{" "}
- {((createMutation.error || confirmMutation.error) as Error).message}
+ {t("warehouse.outbound.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })}
)}
{hasStockIssues && (
}>
- Attenzione: alcune righe superano la disponibilità in magazzino
+ {t("warehouse.outbound.warnings.stockIssues")}
)}
{/* Form Header */}
- Dati Movimento
+ {t("warehouse.outbound.sections.movementData")}
- Magazzino
+ {t("warehouse.outbound.fields.warehouse")}
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")}
} onClick={handleAddLine}>
- Aggiungi Riga
+ {t("warehouse.outbound.actions.addLine")}
@@ -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 */}
- navigate(-1)}>Annulla
+ navigate(-1)}>{t("warehouse.outbound.actions.cancel")}
handleSubmit(false)}
disabled={isPending}
>
- Salva Bozza
+ {t("warehouse.outbound.actions.saveDraft")}
- Salva e Conferma
+ {t("warehouse.outbound.actions.saveAndConfirm")}
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")}
}
onClick={nav.goToValuation}
>
- Valorizzazione
+ {t("warehouse.stockLevels.valuation")}
@@ -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")}
setWarehouseId(e.target.value as number | "")}
>
- Tutti
+ {t("warehouse.stockLevels.filters.allWarehouses")}
{warehouses?.map((w) => (
@@ -304,14 +306,14 @@ export default function StockLevelsPage() {
- Categoria
+ {t("warehouse.stockLevels.filters.category")}
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")}
setSourceWarehouseId(e.target.value as number)
}
@@ -251,10 +252,10 @@ export default function TransferMovementPage() {
- Magazzino Destinazione
+ {t("warehouse.transfer.fields.destWarehouse")}
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")}
} onClick={handleAddLine}>
- Aggiungi
+ {t("warehouse.transfer.actions.add")}
@@ -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 */}
- navigate(-1)}>Annulla
+ navigate(-1)}>{t("warehouse.transfer.actions.cancel")}
handleSubmit(false)}
disabled={isPending}
>
- Salva Bozza
+ {t("warehouse.transfer.actions.saveDraft")}
handleSubmit(true)}
disabled={isPending}
>
- Salva e Conferma
+ {t("warehouse.transfer.actions.saveAndConfirm")}
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")}
}
onClick={nav.goToStockLevels}
>
- Giacenze
+ {t("warehouse.dashboard.stockLevels")}
@@ -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")}
}
onClick={nav.goToMovements}
>
- Vedi tutti
+ {t("warehouse.dashboard.viewAll")}
{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={
- Gestisci
+ {t("warehouse.dashboard.manage")}
}
>
- {pendingMovements.length} movimenti in bozza
- da confermare
+ {pendingMovements.length} {t("warehouse.dashboard.draftMovements")}
)}
@@ -357,12 +358,11 @@ export default function WarehouseDashboard() {
icon={ }
action={
- Visualizza
+ {t("warehouse.dashboard.view")}
}
>
- {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")}
}
onClick={nav.goToArticles}
>
- Vedi tutti
+ {t("warehouse.dashboard.viewAll")}
{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")}
}
onClick={() => handleOpenDialog()}
>
- Nuovo Magazzino
+ {t("warehouse.locations.newWarehouse")}
@@ -231,7 +233,7 @@ export default function WarehouseLocationsPage() {
- Nessun magazzino configurato
+ {t("warehouse.locations.emptyState.title")}
handleOpenDialog()}
sx={{ mt: 2 }}
>
- Aggiungi il primo magazzino
+ {t("warehouse.locations.emptyState.action")}
@@ -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")}
handleChange("type", e.target.value)}
>
- {Object.entries(warehouseTypeLabels).map(([value, label]) => (
+ {Object.entries(warehouseTypeLabels).map(([value]) => (
- {label}
+ {t(`warehouse.warehouseType.${WarehouseType[parseInt(value, 10)]}`)}
))}
@@ -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")}
/>
- Annulla
+ {t("warehouse.locations.dialog.actions.cancel")}
{createMutation.isPending || updateMutation.isPending
- ? "Salvataggio..."
- : "Salva"}
+ ? t("warehouse.locations.dialog.actions.saving")
+ : t("warehouse.locations.dialog.actions.save")}
@@ -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")}
- setDeleteDialogOpen(false)}>Annulla
+ setDeleteDialogOpen(false)}>{t("warehouse.locations.deleteDialog.cancel")}
- {deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
+ {deleteMutation.isPending ? t("warehouse.locations.deleteDialog.deleting") : t("warehouse.locations.deleteDialog.delete")}
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")}
}
onClick={() => setOpenDialog(true)}
>
- Nuovo Articolo
+ {t("articles.newArticle")}
@@ -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,
@@ -267,10 +269,10 @@ export default function ArticoliPage() {
- Categoria
+ {t("articles.category")}
setFormData({
...formData,
@@ -288,7 +290,7 @@ export default function ArticoliPage() {
@@ -313,7 +315,7 @@ export default function ArticoliPage() {
- Annulla
+ {t("common.cancel")}
- {editingId ? "Salva" : "Crea"}
+ {editingId ? t("common.save") : t("common.create")}
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")}
}
onClick={() => refetch()}
>
- Aggiorna
+ {t("common.refresh")}
@@ -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")}
- setConfirmReset(null)}>Annulla
+ setConfirmReset(null)}>{t("common.cancel")}
- Reset
+ {t("autoCodes.reset")}
@@ -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")}
- setPreviewCode(null)}>Chiudi
+ setPreviewCode(null)}>{t("common.close")}
@@ -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() {
- setShowHelp(false)}>Chiudi
+ setShowHelp(false)}>{t("common.close")}
@@ -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")}
{
const value = e.target.value;
setFormData({
@@ -708,9 +709,9 @@ function EditConfigDialog({
});
}}
>
- Mai
- Ogni anno
- Ogni mese
+ {t("autoCodes.never")}
+ {t("autoCodes.everyYear")}
+ {t("autoCodes.everyMonth")}
@@ -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")}
)}
- Annulla
+ {t("common.cancel")}
: null
}
>
- Salva
+ {t("common.save")}
>
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} ?
- Annulla
+ {t("common.cancel")}
- Crea Evento
+ {t("calendar.createEvent")}
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")}
}
onClick={() => setOpenDialog(true)}
>
- Nuovo Cliente
+ {t("clients.newClient")}
@@ -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() {
- Annulla
+ {t("common.cancel")}
- {editingId ? "Salva" : "Crea"}
+ {editingId ? t("common.save") : t("common.create")}
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")}
+
+
+
+ }
+ onClick={() => setIsCreating(true)}
+ >
+ {t("customFields.newField")}
+
+ }
+ onClick={() => refetch()}
+ >
+ {t("common.refresh")}
+
+
+
-
-
-
-
-
- Entità
- setSelectedEntity(e.target.value)}
+ {/* Accordion per entità */}
+ {Object.entries(entityNames).map(([entityCode, entityName]) => {
+ const entityConfigs = groupedConfigs[entityCode] || [];
+ return (
+
+ setExpandedEntity(isExpanded ? entityCode : false)
+ }
+ sx={{ mb: 1 }}
+ >
+ }>
+
+ {getEntityIcon(entityCode)}
+
+ {t(`customFields.entities.${entityCode.toLowerCase()}`) || entityName}
+
+
+
+
+
+ {entityConfigs.length === 0 ? (
+
- {ENTITIES.map(e => (
- {e.label}
+ {t("customFields.noFields")}
+
+ ) : (
+
+
+
+
+ {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")}
+
+
+
+ setConfirmDelete(null)}>
+ {t("common.cancel")}
+
+
+ confirmDelete && deleteMutation.mutate(confirmDelete)
+ }
+ disabled={deleteMutation.isPending}
+ startIcon={
+ deleteMutation.isPending ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {t("common.delete")}
+
+
+
+
+ );
+}
+
+// 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")}
+
+ setFormData({ ...formData, entityName: e.target.value })
+ }
+ >
+ {Object.entries(entityNames).map(([code, name]) => (
+
+ {t(`customFields.entities.${code.toLowerCase()}`) || name}
+
))}
-
-
- }
- onClick={() => {
- setCurrentField({
- type: CustomFieldType.Text,
- isRequired: false,
- entityName: selectedEntity
- });
- setDialogOpen(true);
- }}
- >
- Nuovo Campo
-
-
-
-
-
+
+ )}
-
-
-
-
- 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")}
setCurrentField({ ...currentField, type: e.target.value as CustomFieldType })}
+ value={formData.type ?? CustomFieldType.Text}
+ label={t("customFields.type")}
+ onChange={(e) =>
+ setFormData({
+ ...formData,
+ type: Number(e.target.value) as CustomFieldType,
+ })
+ }
+ disabled={!isCreating}
>
- {FIELD_TYPES.map(t => (
- {t.label}
- ))}
+ {Object.entries(fieldTypeNames).map(([type]) => {
+ const typeEnum = Number(type) as CustomFieldType;
+ return (
+
+ {t(`customFields.types.${CustomFieldType[typeEnum].toLowerCase()}`)}
+
+ );
+ })}
+
- {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")}
/>
-
-
-
- setDialogOpen(false)}>Annulla
- Salva
-
-
+
+
-
-
- {snackbar.message}
-
-
-
+ {error && (
+
+ {error.message || t("common.error")}
+
+ )}
+
+
+ {t("common.cancel")}
+ : null
+ }
+ >
+ {t("common.save")}
+
+
+
);
};
-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")}
}
onClick={() => setDemoDialog("generate")}
>
- Genera Dati Demo
+ {t("dashboard.generateDemoData")}
}
onClick={() => setDemoDialog("clear")}
>
- Pulisci Database
+ {t("dashboard.clearDatabase")}
@@ -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 ? "Chiudi" : "Annulla"}
+ {result ? t("common.close") : t("common.cancel")}
{!result && (
- Genera
+ {t("common.generate")}
)}
@@ -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 ? "Chiudi" : "Annulla"}
+ {result ? t("common.close") : t("common.cancel")}
{!result && (
- Elimina Tutto
+ {t("common.deleteAll")}
)}
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')}
} onClick={() => setOpenDialog(true)}>
- Nuovo Evento
+ {t('events.newEvent')}
@@ -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')}
setFormData({ ...formData, clienteId: e.target.value as number })}
>
{clienti.map((c) => (
@@ -229,10 +231,10 @@ export default function EventiPage() {
- Location
+ {t('events.location')}
setFormData({ ...formData, locationId: e.target.value as number })}
>
{locations.map((l) => (
@@ -241,10 +243,10 @@ export default function EventiPage() {
- Tipo Evento
+ {t('events.type')}
setFormData({ ...formData, tipoEventoId: e.target.value as number })}
>
{tipiEvento.map((t) => (
@@ -253,7 +255,7 @@ export default function EventiPage() {
- setOpenDialog(false)}>Annulla
- Crea
+ setOpenDialog(false)}>{t('common.cancel')}
+ {t('common.create')}
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")}
ricalcolaQuantitaMutation.mutate()}
size="small"
>
- Ricalcola Qta
+ {t("events.detail.actions.recalculate")}
{data.stato !== StatoEvento.Confermato && (
- Conferma
+ {t("events.detail.actions.confirm")}
)}
>
@@ -417,7 +419,7 @@ export default function EventoDetailPage() {
onClick={handleSave}
disabled={!hasChanges && !isNew}
>
- Salva
+ {t("events.detail.actions.save")}
{!isNew && (
- Stampa PDF
+ {t("events.detail.actions.print")}
)}
@@ -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")}
handleFieldChange("tipoEventoId", e.target.value)
}
@@ -495,12 +497,12 @@ export default function EventoDetailPage() {
handleFieldChange("descrizione", e.target.value)}
- placeholder="es. Matrimonio Rossi-Bianchi"
+ placeholder={t("events.detail.fields.descriptionPlaceholder")}
/>
@@ -514,7 +516,7 @@ export default function EventoDetailPage() {
handleFieldChange("clienteId", newValue?.id)
}
renderInput={(params) => (
-
+
)}
/>
@@ -529,7 +531,7 @@ export default function EventoDetailPage() {
renderInput={(params) => (
@@ -540,7 +542,7 @@ export default function EventoDetailPage() {
{/* Terza riga: Dati economici */}
- Stato
+ {t("events.detail.fields.status")}
{
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}
}
@@ -671,7 +673,7 @@ export default function EventoDetailPage() {
setDialogOpen("ospite");
}}
>
- Aggiungi Tipo Ospite
+ {t("events.detail.guestsTab.add")}
@@ -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}
- Aggiungi Articolo
+ {t("events.detail.withdrawalTab.add")}
@@ -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}
- Aggiungi Risorsa
+ {t("events.detail.resourcesTab.add")}
@@ -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")}
setDialogData({ ...dialogData, tipoOspiteId: e.target.value })
}
@@ -999,7 +998,7 @@ export default function EventoDetailPage() {
- setDialogOpen(null)}>Annulla
+ setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}
addOspiteMutation.mutate(dialogData)}
>
- Aggiungi
+ {t("events.detail.dialogs.add")}
@@ -1040,7 +1039,7 @@ export default function EventoDetailPage() {
maxWidth="sm"
fullWidth
>
- Aggiungi Articolo alla Lista
+ {t("events.detail.dialogs.addArticle")}
(
-
+
)}
/>
- setDialogOpen(null)}>Annulla
+ setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}
addPrelievoMutation.mutate(dialogData)}
>
- Aggiungi
+ {t("events.detail.dialogs.add")}
@@ -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() {
- setDialogOpen(null)}>Annulla
+ setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}
addRisorsaMutation.mutate(dialogData)}
>
- Aggiungi
+ {t("events.detail.dialogs.add")}
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')}
} onClick={() => setOpenDialog(true)}>
- Nuova Location
+ {t('location.newLocation')}
@@ -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() {
- Annulla
+ {t('common.cancel')}
- {editingId ? 'Salva' : 'Crea'}
+ {editingId ? t('common.save') : t('common.create')}
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")}
}
onClick={() => navigate("/")}
>
- Torna alla Home
+ {t("modules.admin.backToHome")}
);
@@ -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")}
}
onClick={() => refreshModules()}
>
- Aggiorna
+ {t("modules.admin.refresh")}
@@ -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() {
)}
- setSelectedModule(null)}>Chiudi
+ setSelectedModule(null)}>
+ {t("common.close")}
+
>
)}
@@ -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")}
)}
- setConfirmDisable(null)}>Annulla
+ setConfirmDisable(null)}>
+ {t("common.cancel")}
+
- Disattiva
+ {t("modules.admin.disable")}
@@ -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")}
setTemplateInfo((prev) => ({
...prev,
@@ -2278,17 +2280,17 @@ export default function ReportEditorPage() {
}))
}
>
- Generale
- Evento
- Cliente
- Articoli
+ {t("reports.categories.Generale")}
+ {t("reports.categories.Evento")}
+ {t("reports.categories.Cliente")}
+ {t("reports.categories.Articoli")}
setSaveDialog(false)} fullWidth={isMobile}>
- Annulla
+ {t("reports.editor.saveDialog.cancel")}
- {saveMutation.isPending ? "Salvataggio..." : "Salva"}
+ {saveMutation.isPending ? t("reports.editor.saveDialog.saving") : t("reports.editor.saveDialog.save")}
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")}
}
onClick={() => navigate("/report-editor")}
>
- Nuovo Template
+ {t("reports.newTemplate")}
)}
@@ -196,16 +198,16 @@ export default function ReportTemplatesPage() {
}}
>
- Filtra per categoria
+ {t("reports.filterCategory")}
setFilterCategoria(e.target.value)}
>
- Tutte
+ {t("reports.all")}
{categories.map((cat) => (
- {cat}
+ {t(`reports.categories.${cat}`) || cat}
))}
@@ -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")}
navigate("/report-editor")}
fullWidth={isMobile}
>
- Crea Template
+ {t("reports.createTemplate")}
@@ -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")}
- {deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
+ {deleteMutation.isPending ? t("reports.deleting") : t("reports.delete")}
@@ -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")}
- {importFile ? importFile.name : "Seleziona File"}
+ {importFile ? importFile.name : t("reports.selectFile")}
- Annulla
+ {t("reports.cancel")}
- {importMutation.isPending ? "Importazione..." : "Importa"}
+ {importMutation.isPending ? t("reports.importing") : t("reports.import")}
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')}
} onClick={() => setOpenDialog(true)}>
- Nuova Risorsa
+ {t('resources.newResource')}
@@ -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')}
setFormData({ ...formData, tipoRisorsaId: e.target.value as number })}
>
{tipiRisorsa.map((t) => (
@@ -175,7 +177,7 @@ export default function RisorsePage() {
setFormData({ ...formData, telefono: e.target.value })}
@@ -183,7 +185,7 @@ export default function RisorsePage() {
- Annulla
+ {t('common.cancel')}
- {editingId ? 'Salva' : 'Crea'}
+ {editingId ? t('common.save') : t('common.create')}
diff --git a/frontend/src/types/customFields.ts b/frontend/src/types/customFields.ts
index 435dee5..6ccdd32 100644
--- a/frontend/src/types/customFields.ts
+++ b/frontend/src/types/customFields.ts
@@ -30,3 +30,45 @@ export interface CustomFieldDefinition {
export interface CustomFieldValues {
[key: string]: any;
}
+
+export const entityNames: Record = {
+ Client: "Clienti",
+ Article: "Articoli",
+ Event: "Eventi",
+ WarehouseArticle: "Articoli Magazzino",
+ WarehouseLocation: "Magazzini",
+ Resource: "Risorse"
+};
+
+export const entityIcons: Record = {
+ Client: "People",
+ Article: "Restaurant",
+ Event: "Event",
+ WarehouseArticle: "Inventory",
+ WarehouseLocation: "Warehouse",
+ Resource: "Badge"
+};
+
+export const fieldTypeNames: Record = {
+ [CustomFieldType.Text]: "Testo",
+ [CustomFieldType.Number]: "Numero",
+ [CustomFieldType.Date]: "Data",
+ [CustomFieldType.Boolean]: "Booleano",
+ [CustomFieldType.Select]: "Selezione",
+ [CustomFieldType.MultiSelect]: "Selezione Multipla",
+ [CustomFieldType.TextArea]: "Area Testo",
+ [CustomFieldType.Color]: "Colore",
+ [CustomFieldType.Url]: "URL",
+ [CustomFieldType.Email]: "Email"
+};
+
+export const groupByEntity = (fields: CustomFieldDefinition[]) => {
+ return fields.reduce((acc, field) => {
+ const entity = field.entityName;
+ if (!acc[entity]) {
+ acc[entity] = [];
+ }
+ acc[entity].push(field);
+ return acc;
+ }, {} as Record);
+};