feat: implement global translation for HR, purchases, and core UI components

This commit is contained in:
2025-12-06 02:01:54 +01:00
parent fef463dce5
commit 623f7b3b56
19 changed files with 789 additions and 361 deletions

View File

@@ -42,5 +42,8 @@ File riassuntivo dello stato di sviluppo di Zentral.
- Miglioramento UX tab: chiusura con middle-click, drag & drop, gruppi di tab personalizzati. - Miglioramento UX tab: chiusura con middle-click, drag & drop, gruppi di tab personalizzati.
- [2025-12-06 Tab Flicker Fix](./devlog/2025-12-06-011500_tab_flicker_fix.md) - **Completato** - [2025-12-06 Tab Flicker Fix](./devlog/2025-12-06-011500_tab_flicker_fix.md) - **Completato**
- Risolto problema di flicker rimuovendo l'aggiornamento manuale dello stato attivo e affidandosi esclusivamente alla sincronizzazione con l'URL. - Risolto problema di flicker rimuovendo l'aggiornamento manuale dello stato attivo e affidandosi esclusivamente alla sincronizzazione con l'URL.
- [2025-12-06 Fix Apps Tab Translation](./devlog/2025-12-06-013500_fix_apps_tab_translation.md) - **Completato** - [2025-12-06 02:10:00 - Fix Traduzione Tab](./devlog/2025-12-06-021000_fix_tab_translation.md) - **Completato**
- [2025-12-06 01:55:00 - Traduzione Menu, Search Bar e Tab](./devlog/2025-12-06-015500_translate_navigation.md) - **Completato**
- [2025-12-06 01:48:00 - Traduzione Modulo Acquisti](./devlog/2025-12-06-014800_translate_purchases.md) - **Completato**
- [2025-12-06 01:35:00 - Fix Traduzione Tab Applicazioni](./devlog/2025-12-06-013500_fix_apps_tab_translation.md) - **Completato**
- Corretta chiave di traduzione errata per la tab "Gestione Applicazioni" e migliorata la gestione dell'aggiornamento etichette tab. - Corretta chiave di traduzione errata per la tab "Gestione Applicazioni" e migliorata la gestione dell'aggiornamento etichette tab.

View File

@@ -0,0 +1,18 @@
# Global Translation Alignment
## Stato
Completato
## Descrizione
Allineamento completo delle traduzioni in tutto il gestionale. Verifica di stringhe hardcoded, chiavi mancanti e supporto accessibilità.
## Piano di Lavoro
1. [x] Analisi struttura i18n esistente.
2. [x] Scansione frontend per stringhe hardcoded.
3. [x] Scansione backend per messaggi utente non localizzati.
4. [x] Aggiornamento file di traduzione (IT/EN).
5. [x] Verifica accessibilità (aria-labels, alt text).
6. [x] Test cambio lingua.
## Log
- Creazione piano di lavoro.

View File

@@ -0,0 +1,17 @@
# Traduzione Modulo Acquisti
## Obiettivo
Tradurre completamente il modulo acquisti (Purchases) in italiano e inglese, eliminando le stringhe hardcoded.
## File da analizzare
- `src/frontend/src/apps/purchases/pages/PurchaseOrderFormPage.tsx`
- `src/frontend/src/apps/purchases/pages/PurchaseOrdersPage.tsx`
- `src/frontend/src/apps/purchases/pages/SupplierFormPage.tsx`
- `src/frontend/src/apps/purchases/pages/SuppliersPage.tsx`
- `src/frontend/src/apps/purchases/components/PurchasesStatsWidget.tsx`
## Piano di lavoro
1. Analizzare i file per identificare le stringhe hardcoded.
2. Aggiungere le chiavi di traduzione in `it/translation.json` e `en/translation.json`.
3. Aggiornare i componenti React per utilizzare `useTranslation`.
4. Verificare la build.

View File

@@ -0,0 +1,21 @@
# Traduzione Menu, Search Bar e Tab
## Obiettivo
Tradurre completamente i componenti di navigazione principale: Sidebar (Menu), Search Bar e TabsBar.
## File da analizzare
- `src/frontend/src/components/Sidebar.tsx`
- `src/frontend/src/components/SearchBar.tsx`
- `src/frontend/src/components/TabsBar.tsx`
## Piano di lavoro
1. Analizzare `Sidebar.tsx` per le voci di menu hardcoded.
2. Analizzare `SearchBar.tsx` per placeholder e testi hardcoded.
3. Analizzare `TabsBar.tsx` per i titoli delle tab e menu contestuali.
4. Aggiungere le chiavi mancanti in `it/translation.json` e `en/translation.json`.
5. Aggiornare i componenti per usare `useTranslation`.
## Stato
- **Completato**: 2025-12-06 02:05:00
- Aggiunte chiavi di traduzione per menu, navigazione e tab.
- Aggiornati i componenti `Sidebar.tsx`, `SearchBar.tsx` e `TabsBar.tsx`.

View File

@@ -0,0 +1,26 @@
# Traduzione Tab
## Problema
Le tab aperte non venivano tradotte dinamicamente al cambio lingua perché il titolo (label) veniva salvato come stringa statica nel `TabContext` (e persistito in localStorage).
## Soluzione
1. Aggiornato `TabContext.tsx`:
- Aggiunta proprietà opzionale `translationKey` all'interfaccia `Tab`.
- Aggiornata la funzione `openTab` per accettare e salvare `translationKey`.
- Aggiornato il caricamento iniziale (default tab) per includere la chiave di traduzione.
2. Aggiornato `Sidebar.tsx`:
- Aggiunta proprietà `translationKey` alla struttura del menu.
- Passaggio della chiave di traduzione alla funzione `openTab` al click.
3. Aggiornato `SearchBar.tsx`:
- Aggiunta proprietà `translationKey` alle opzioni di ricerca.
- Passaggio della chiave di traduzione alla funzione `openTab` alla selezione.
4. Aggiornato `TabsBar.tsx`:
- Utilizzo di `t(tab.translationKey)` se disponibile, altrimenti fallback su `tab.label`.
- Questo garantisce che le tab cambino lingua istantaneamente quando l'utente cambia lingua.
## Stato
- **Completato**: 2025-12-06 02:15:00
- Le tab ora supportano la traduzione dinamica.

View File

@@ -51,7 +51,32 @@
"reports": "Reports", "reports": "Reports",
"apps": "Apps", "apps": "Apps",
"autoCodes": "Auto Codes", "autoCodes": "Auto Codes",
"customFields": "Custom Fields" "customFields": "Custom Fields",
"suppliers": "Suppliers",
"purchaseOrders": "Purchase Orders",
"salesOrders": "Sales Orders",
"productionOrders": "Production Orders",
"bom": "Bill of Materials",
"workCenters": "Work Centers",
"cycles": "Cycles",
"mrp": "MRP",
"administration": "Administration",
"movements": "Movements",
"stock": "Stock",
"inventory": "Inventory"
},
"navigation": {
"searchPlaceholder": "Search...",
"tabGroups": "Tab Groups",
"close": "Close",
"closeOthers": "Close Others",
"closeRight": "Close to the Right",
"saveSession": "Save Current Session",
"noSavedGroups": "No saved groups",
"saveGroupTitle": "Save Tab Group",
"groupName": "Group Name",
"save": "Save",
"cancel": "Cancel"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
@@ -515,6 +540,135 @@
"saving": "Saving...", "saving": "Saving...",
"save": "Save" "save": "Save"
} }
},
"elements": {
"text": "Text",
"textDesc": "Add a text field",
"image": "Image",
"imageDesc": "Insert an image",
"shape": "Shape",
"shapeDesc": "Draw a geometric shape",
"table": "Table",
"tableDesc": "Insert a data table",
"line": "Line",
"lineDesc": "Draw a line",
"add": "Add",
"insert": "Insert element"
},
"snap": {
"grid": "Grid",
"objects": "Objects",
"borders": "Margins",
"center": "Center",
"tangent": "Edges",
"options": "Snap Options",
"all": "All",
"hideGrid": "Hide grid",
"showGrid": "Show grid",
"autoAlign": "Auto alignment"
},
"toolbar": {
"undo": "Undo",
"redo": "Redo",
"delete": "Delete",
"preview": "Preview",
"save": "Save",
"lock": "Lock",
"unlock": "Unlock",
"duplicate": "Duplicate",
"prevPage": "Previous Page",
"nextPage": "Next Page",
"zoomIn": "Zoom in",
"zoomOut": "Zoom out",
"autoSaveOn": "Auto-save on",
"autoSaveOff": "Auto-save off",
"saving": "Saving...",
"saved": "Saved",
"unsaved": "Unsaved",
"unsavedTooltip": "Unsaved changes",
"autoSavePending": "Auto-save pending...",
"edit": "EDIT",
"history": "HISTORY",
"historyTooltip": "Change history",
"view": "VIEW",
"zoom": "ZOOM",
"fitWindow": "Fit to window",
"zoomLevel": "Zoom level",
"presets": "Presets",
"searchCommand": "Search command",
"shortcuts": "Keyboard shortcuts",
"shortcutsTitle": "Keyboard Shortcuts"
},
"preview": {
"title": "Report Preview",
"notSelected": "Not selected",
"removeSelection": "Remove selection",
"select": "Select",
"searchPlaceholder": "Search...",
"noResults": "No results found",
"noEntities": "No entities available",
"results": "results",
"selected": "selected",
"instruction": "Select an entity for each dataset to use in the preview",
"errorLoading": "Error loading available data",
"noDatasets": "There are no datasets selected for this template. Add at least one dataset to generate the preview.",
"selectEntityInstruction": "Select an entity for each dataset",
"cancel": "Cancel",
"generating": "Generating...",
"generatePdf": "Generate PDF",
"generatePreviewPdf": "Generate PDF Preview"
},
"datasetManager": {
"title": "Virtual Datasets",
"newDataset": "New Dataset",
"noDatasets": "No Virtual Datasets",
"noDatasetsDesc": "Create virtual datasets to combine and filter data from multiple sources.",
"createFirst": "Create the first dataset",
"editDataset": "Edit Dataset",
"newVirtualDataset": "New Virtual Dataset",
"deleteConfirm": "Delete dataset \"{{name}}\"?",
"validationError": "Validation error",
"errors": "Errors:",
"warnings": "Warnings:",
"validConfig": "Valid configuration",
"tabs": {
"info": "Info",
"sources": "Sources",
"relationships": "Relationships",
"filters": "Filters",
"fields": "Fields"
},
"fields": {
"nameId": "Identifier Name",
"nameIdHelper": "Unique name used internally (no spaces)",
"displayName": "Display Name",
"description": "Description",
"category": "Category",
"icon": "Icon"
},
"sources": {
"available": "Available Datasets",
"addInstruction": "Click to add a source",
"inDataset": "Sources in Dataset",
"empty": "Add at least one data source from the left panel",
"alias": "Alias",
"primary": "Primary",
"setPrimary": "Set Primary"
},
"noDescription": "No description",
"sourcesCount": "sources"
},
"shortcuts": {
"move1px": "Move (1px)",
"move10px": "Move (10px)",
"toggleGrid": "Toggle grid",
"zoomInOut": "Zoom in/out",
"changePage": "Change page"
},
"time": {
"now": "Just now",
"minutesAgo": "{{count}}m ago",
"hoursAgo": "{{count}}h ago"
} }
}, },
"warehouse": { "warehouse": {
@@ -1072,17 +1226,19 @@
} }
}, },
"purchases": { "purchases": {
"menu": { "stats": {
"suppliers": "Suppliers", "title": "Purchases",
"orders": "Purchase Orders" "costsThisMonth": "Costs this month",
"pendingOrders": "{{count}} Pending Orders"
}, },
"suppliers": { "supplier": {
"title": "Suppliers", "title": "Suppliers",
"newSupplier": "New Supplier", "newSupplier": "New Supplier",
"editSupplier": "Edit Supplier", "createTitle": "New Supplier",
"editTitle": "Edit Supplier",
"columns": { "columns": {
"code": "Code", "code": "Code",
"name": "Name", "name": "Business Name",
"vatNumber": "VAT Number", "vatNumber": "VAT Number",
"email": "Email", "email": "Email",
"phone": "Phone", "phone": "Phone",
@@ -1090,85 +1246,60 @@
"status": "Status" "status": "Status"
}, },
"fields": { "fields": {
"code": "Code",
"name": "Business Name", "name": "Business Name",
"vatNumber": "VAT Number", "vatNumber": "VAT Number",
"fiscalCode": "Fiscal Code", "fiscalCode": "Fiscal Code",
"email": "Email",
"pec": "PEC",
"phone": "Phone",
"website": "Website",
"address": "Address", "address": "Address",
"city": "City", "city": "City",
"province": "Province", "province": "Province",
"zipCode": "ZIP Code", "zipCode": "ZIP Code",
"country": "Country", "country": "Country",
"email": "Email",
"pec": "PEC",
"phone": "Phone",
"website": "Website",
"paymentTerms": "Payment Terms", "paymentTerms": "Payment Terms",
"notes": "Notes", "notes": "Notes"
"isActive": "Active" }
},
"placeholders": {
"search": "Search supplier...",
"generatedAutomatically": "Generated automatically"
},
"deleteConfirm": "Are you sure you want to delete this supplier?"
}, },
"orders": { "order": {
"title": "Purchase Orders", "title": "Purchase Orders",
"newOrder": "New Order", "newOrder": "New Order",
"editOrder": "Edit Order", "createTitle": "New Order",
"columns": { "editTitle": "Edit Order",
"orderNumber": "Order Number",
"orderDate": "Date",
"supplier": "Supplier",
"status": "Status",
"total": "Total",
"deliveryDate": "Delivery Date"
},
"fields": {
"orderNumber": "Order Number",
"orderDate": "Order Date",
"expectedDeliveryDate": "Expected Delivery",
"supplier": "Supplier",
"destinationWarehouse": "Destination Warehouse",
"notes": "Notes",
"article": "Article",
"quantity": "Quantity",
"unitPrice": "Unit Price",
"discount": "Discount %",
"taxRate": "Tax Rate %",
"lineTotal": "Total"
},
"status": { "status": {
"Draft": "Draft", "Draft": "Draft",
"Confirmed": "Confirmed", "Confirmed": "Confirmed",
"PartiallyReceived": "Partially Received",
"Received": "Received", "Received": "Received",
"Cancelled": "Cancelled" "Cancelled": "Cancelled"
}, },
"columns": {
"number": "Number",
"date": "Date",
"supplier": "Supplier",
"status": "Status",
"total": "Total"
},
"fields": {
"date": "Order Date",
"expectedDate": "Expected Delivery Date",
"supplier": "Supplier",
"warehouse": "Destination Warehouse",
"notes": "Notes"
},
"lines": {
"article": "Article",
"quantity": "Quantity",
"price": "Unit Price",
"discount": "Discount %",
"tax": "Tax %",
"total": "Total"
},
"total": "Order Total",
"actions": { "actions": {
"addLine": "Add Line",
"confirm": "Confirm Order", "confirm": "Confirm Order",
"receive": "Receive Goods", "receive": "Receive Goods"
"view": "View",
"delete": "Delete"
},
"totals": {
"net": "Net Total",
"tax": "Tax",
"gross": "Gross Total"
},
"deleteConfirm": "Are you sure you want to delete this order?",
"confirmDialog": {
"title": "Confirm Order",
"content": "Are you sure you want to confirm this order? It will no longer be editable.",
"confirm": "Confirm",
"cancel": "Cancel"
},
"receiveDialog": {
"title": "Receive Goods",
"content": "Are you sure you want to mark this order as received? This will generate stock movements.",
"confirm": "Receive",
"cancel": "Cancel"
} }
} }
}, },
@@ -1393,6 +1524,21 @@
"rimborsiTitle": "Reimbursement Management", "rimborsiTitle": "Reimbursement Management",
"newRimborso": "New Reimbursement", "newRimborso": "New Reimbursement",
"editRimborso": "Edit Reimbursement", "editRimborso": "Edit Reimbursement",
"descrizione": "Description" "descrizione": "Description",
"status": {
"richiesto": "Requested",
"approvato": "Approved",
"rimborsato": "Reimbursed",
"rifiutato": "Rejected",
"richiesta": "Requested",
"approvata": "Approved",
"rifiutata": "Rejected"
},
"assenza": {
"ferie": "Vacation",
"malattia": "Sick Leave",
"permesso": "Permit",
"altro": "Other"
}
} }
} }

View File

@@ -47,7 +47,32 @@
"reports": "Report", "reports": "Report",
"apps": "Applicazioni", "apps": "Applicazioni",
"autoCodes": "Codici Auto", "autoCodes": "Codici Auto",
"customFields": "Campi Personalizzati" "customFields": "Campi Personalizzati",
"suppliers": "Fornitori",
"purchaseOrders": "Ordini Acquisto",
"salesOrders": "Ordini Vendita",
"productionOrders": "Ordini Produzione",
"bom": "Distinte Base",
"workCenters": "Centri di Lavoro",
"cycles": "Cicli",
"mrp": "MRP",
"administration": "Amministrazione",
"movements": "Movimenti",
"stock": "Giacenze",
"inventory": "Inventario"
},
"navigation": {
"searchPlaceholder": "Cerca...",
"tabGroups": "Gruppi Schede",
"close": "Chiudi",
"closeOthers": "Chiudi Altre",
"closeRight": "Chiudi a Destra",
"saveSession": "Salva Sessione Corrente",
"noSavedGroups": "Nessun gruppo salvato",
"saveGroupTitle": "Salva Gruppo Schede",
"groupName": "Nome Gruppo",
"save": "Salva",
"cancel": "Annulla"
}, },
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
@@ -591,9 +616,143 @@
"saving": "Salvataggio...", "saving": "Salvataggio...",
"save": "Salva" "save": "Salva"
} }
},
"elements": {
"text": "Testo",
"textDesc": "Aggiungi un campo di testo",
"image": "Immagine",
"imageDesc": "Inserisci un'immagine",
"shape": "Forma",
"shapeDesc": "Disegna una forma geometrica",
"table": "Tabella",
"tableDesc": "Inserisci una tabella dati",
"line": "Linea",
"lineDesc": "Traccia una linea",
"add": "Aggiungi",
"insert": "Inserisci elemento"
},
"snap": {
"grid": "Griglia",
"objects": "Oggetti",
"borders": "Margini",
"center": "Centro",
"tangent": "Bordi",
"options": "Opzioni Snap",
"all": "Tutti",
"hideGrid": "Nascondi griglia",
"showGrid": "Mostra griglia",
"autoAlign": "Allineamento automatico"
},
"toolbar": {
"undo": "Annulla",
"redo": "Ripeti",
"delete": "Elimina",
"preview": "Anteprima",
"save": "Salva",
"lock": "Blocca",
"unlock": "Sblocca",
"duplicate": "Duplica",
"prevPage": "Pagina precedente",
"nextPage": "Pagina successiva",
"zoomIn": "Zoom in",
"zoomOut": "Zoom out",
"autoSaveOn": "Auto-salvataggio attivo",
"autoSaveOff": "Auto-salvataggio disattivato",
"saving": "Salvataggio in corso...",
"saved": "Salvato",
"unsaved": "Non salvato",
"unsavedTooltip": "Modifiche non salvate",
"autoSavePending": "Salvataggio automatico in attesa...",
"edit": "MODIFICA",
"history": "CRONOLOGIA",
"historyTooltip": "Cronologia modifiche",
"view": "VISTA",
"zoom": "ZOOM",
"fitWindow": "Adatta alla finestra",
"zoomLevel": "Livello zoom",
"presets": "Preset",
"searchCommand": "Cerca comando",
"shortcuts": "Scorciatoie tastiera",
"shortcutsTitle": "Scorciatoie Tastiera"
},
"preview": {
"title": "Anteprima Report",
"notSelected": "Non selezionato",
"removeSelection": "Rimuovi selezione",
"select": "Seleziona",
"searchPlaceholder": "Cerca...",
"noResults": "Nessun risultato trovato",
"noEntities": "Nessuna entità disponibile",
"results": "risultati",
"selected": "selezionati",
"instruction": "Seleziona un'entità per ogni dataset da utilizzare nell'anteprima",
"errorLoading": "Errore nel caricamento dei dati disponibili",
"noDatasets": "Non ci sono dataset selezionati per questo template. Aggiungi almeno un dataset per poter generare l'anteprima.",
"selectEntityInstruction": "Seleziona un'entità per ogni dataset",
"cancel": "Annulla",
"generating": "Generazione...",
"generatePdf": "Genera PDF",
"generatePreviewPdf": "Genera Anteprima PDF"
},
"datasetManager": {
"title": "Dataset Virtuali",
"newDataset": "Nuovo Dataset",
"noDatasets": "Nessun Dataset Virtuale",
"noDatasetsDesc": "Crea dataset virtuali per combinare e filtrare i dati da più sorgenti.",
"createFirst": "Crea il primo dataset",
"editDataset": "Modifica Dataset",
"newVirtualDataset": "Nuovo Dataset Virtuale",
"deleteConfirm": "Eliminare il dataset \"{{name}}\"?",
"validationError": "Errore durante la validazione",
"errors": "Errori:",
"warnings": "Avvisi:",
"validConfig": "Configurazione valida",
"tabs": {
"info": "Info",
"sources": "Sorgenti",
"relationships": "Relazioni",
"filters": "Filtri",
"fields": "Campi"
},
"fields": {
"nameId": "Nome Identificativo",
"nameIdHelper": "Nome univoco usato internamente (senza spazi)",
"displayName": "Nome Visualizzato",
"description": "Descrizione",
"category": "Categoria",
"icon": "Icona"
},
"sources": {
"available": "Dataset Disponibili",
"addInstruction": "Clicca per aggiungere una sorgente",
"inDataset": "Sorgenti nel Dataset",
"empty": "Aggiungi almeno una sorgente dati dal pannello a sinistra",
"alias": "Alias",
"primary": "Primario",
"setPrimary": "Imposta Primario"
},
"noDescription": "Nessuna descrizione",
"sourcesCount": "sorgenti"
},
"shortcuts": {
"move1px": "Sposta (1px)",
"move10px": "Sposta (10px)",
"toggleGrid": "Mostra/nascondi griglia",
"zoomInOut": "Zoom in/out",
"changePage": "Cambia pagina"
},
"time": {
"now": "Ora",
"minutesAgo": "{{count}}m fa",
"hoursAgo": "{{count}}h fa"
} }
}, },
"purchases": { "purchases": {
"stats": {
"title": "Acquisti",
"costsThisMonth": "Costi questo mese",
"pendingOrders": "{{count}} Ordini in attesa"
},
"supplier": { "supplier": {
"title": "Fornitori", "title": "Fornitori",
"newSupplier": "Nuovo Fornitore", "newSupplier": "Nuovo Fornitore",
@@ -1446,6 +1605,21 @@
"rimborsiTitle": "Gestione Rimborsi", "rimborsiTitle": "Gestione Rimborsi",
"newRimborso": "Nuovo Rimborso", "newRimborso": "Nuovo Rimborso",
"editRimborso": "Modifica Rimborso", "editRimborso": "Modifica Rimborso",
"descrizione": "Descrizione" "descrizione": "Descrizione",
"status": {
"richiesto": "Richiesto",
"approvato": "Approvato",
"rimborsato": "Rimborsato",
"rifiutato": "Rifiutato",
"richiesta": "Richiesta",
"approvata": "Approvata",
"rifiutata": "Rifiutata"
},
"assenza": {
"ferie": "Ferie",
"malattia": "Malattia",
"permesso": "Permesso",
"altro": "Altro"
}
} }
} }

View File

@@ -221,10 +221,10 @@ export default function AssenzePage() {
onChange={(e) => setFormData({ ...formData, tipoAssenza: e.target.value })} onChange={(e) => setFormData({ ...formData, tipoAssenza: e.target.value })}
required required
> >
<MenuItem value="Ferie">Ferie</MenuItem> <MenuItem value="Ferie">{t('personale.assenza.ferie')}</MenuItem>
<MenuItem value="Malattia">Malattia</MenuItem> <MenuItem value="Malattia">{t('personale.assenza.malattia')}</MenuItem>
<MenuItem value="Permesso">Permesso</MenuItem> <MenuItem value="Permesso">{t('personale.assenza.permesso')}</MenuItem>
<MenuItem value="Altro">Altro</MenuItem> <MenuItem value="Altro">{t('personale.assenza.altro')}</MenuItem>
</TextField> </TextField>
</Grid> </Grid>
<Grid size={6}> <Grid size={6}>
@@ -236,9 +236,9 @@ export default function AssenzePage() {
onChange={(e) => setFormData({ ...formData, stato: e.target.value })} onChange={(e) => setFormData({ ...formData, stato: e.target.value })}
required required
> >
<MenuItem value="Richiesta">Richiesta</MenuItem> <MenuItem value="Richiesta">{t('personale.status.richiesta')}</MenuItem>
<MenuItem value="Approvata">Approvata</MenuItem> <MenuItem value="Approvata">{t('personale.status.approvata')}</MenuItem>
<MenuItem value="Rifiutata">Rifiutata</MenuItem> <MenuItem value="Rifiutata">{t('personale.status.rifiutata')}</MenuItem>
</TextField> </TextField>
</Grid> </Grid>
<Grid size={6}> <Grid size={6}>

View File

@@ -243,10 +243,10 @@ export default function RimborsiPage() {
onChange={(e) => setFormData({ ...formData, stato: e.target.value })} onChange={(e) => setFormData({ ...formData, stato: e.target.value })}
required required
> >
<MenuItem value="Richiesto">Richiesto</MenuItem> <MenuItem value="Richiesto">{t('personale.status.richiesto')}</MenuItem>
<MenuItem value="Approvato">Approvato</MenuItem> <MenuItem value="Approvato">{t('personale.status.approvato')}</MenuItem>
<MenuItem value="Rimborsato">Rimborsato</MenuItem> <MenuItem value="Rimborsato">{t('personale.status.rimborsato')}</MenuItem>
<MenuItem value="Rifiutato">Rifiutato</MenuItem> <MenuItem value="Rifiutato">{t('personale.status.rifiutato')}</MenuItem>
</TextField> </TextField>
</Grid> </Grid>
<Grid size={12}> <Grid size={12}>

View File

@@ -1,19 +1,24 @@
import { Card, CardContent, Typography, Box } from '@mui/material'; import { Card, CardContent, Typography, Box } from '@mui/material';
import { ShoppingCart as PurchaseIcon } from '@mui/icons-material'; import { ShoppingCart as PurchaseIcon } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
export default function PurchasesStatsWidget() { export default function PurchasesStatsWidget() {
const { t } = useTranslation();
return ( return (
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent> <CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<PurchaseIcon color="primary" sx={{ mr: 1 }} /> <PurchaseIcon color="primary" sx={{ mr: 1 }} />
<Typography variant="h6">Purchases</Typography> <Typography variant="h6">{t('purchases.stats.title')}</Typography>
</Box> </Box>
<Typography variant="h4" sx={{ mb: 1 }}> 8,320</Typography> <Typography variant="h4" sx={{ mb: 1 }}> 8,320</Typography>
<Typography variant="body2" color="text.secondary">Costs this month</Typography> <Typography variant="body2" color="text.secondary">{t('purchases.stats.costsThisMonth')}</Typography>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="body2" color="warning.main">Pending Orders: 3</Typography> <Typography variant="body2" color="warning.main">
{t('purchases.stats.pendingOrders', { count: 3 })}
</Typography>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -184,11 +184,11 @@ export default function PurchaseOrderFormPage() {
</Button> </Button>
<Box> <Box>
<Typography variant="h4"> <Typography variant="h4">
{isEdit ? `${t("purchases.orders.editOrder")} ${order?.orderNumber}` : t("purchases.orders.newOrder")} {isEdit ? `${t("purchases.order.editTitle")} ${order?.orderNumber}` : t("purchases.order.newOrder")}
</Typography> </Typography>
{isEdit && order && ( {isEdit && order && (
<Chip <Chip
label={t(`purchases.orders.status.${PurchaseOrderStatus[order.status]}`)} label={t(`purchases.order.status.${PurchaseOrderStatus[order.status]}`)}
color={order.status === PurchaseOrderStatus.Confirmed ? "primary" : order.status === PurchaseOrderStatus.Received ? "success" : "default"} color={order.status === PurchaseOrderStatus.Confirmed ? "primary" : order.status === PurchaseOrderStatus.Received ? "success" : "default"}
size="small" size="small"
sx={{ mt: 1 }} sx={{ mt: 1 }}
@@ -206,7 +206,7 @@ export default function PurchaseOrderFormPage() {
onClick={() => confirmMutation.mutate(Number(id))} onClick={() => confirmMutation.mutate(Number(id))}
disabled={confirmMutation.isPending} disabled={confirmMutation.isPending}
> >
{t("purchases.orders.actions.confirm")} {t("purchases.order.actions.confirm")}
</Button> </Button>
)} )}
@@ -218,7 +218,7 @@ export default function PurchaseOrderFormPage() {
onClick={() => receiveMutation.mutate(Number(id))} onClick={() => receiveMutation.mutate(Number(id))}
disabled={receiveMutation.isPending} disabled={receiveMutation.isPending}
> >
{t("purchases.orders.actions.receive")} {t("purchases.order.actions.receive")}
</Button> </Button>
)} )}
@@ -249,7 +249,7 @@ export default function PurchaseOrderFormPage() {
control={control} control={control}
render={({ field }: { field: any }) => ( render={({ field }: { field: any }) => (
<DatePicker <DatePicker
label={t("purchases.orders.fields.orderDate")} label={t("purchases.order.fields.date")}
value={field.value ? dayjs(field.value) : null} value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())} onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly} disabled={isReadOnly}
@@ -264,7 +264,7 @@ export default function PurchaseOrderFormPage() {
control={control} control={control}
render={({ field }: { field: any }) => ( render={({ field }: { field: any }) => (
<DatePicker <DatePicker
label={t("purchases.orders.fields.expectedDeliveryDate")} label={t("purchases.order.fields.expectedDate")}
value={field.value ? dayjs(field.value) : null} value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())} onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly} disabled={isReadOnly}
@@ -288,7 +288,7 @@ export default function PurchaseOrderFormPage() {
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
label={t("purchases.orders.fields.supplier")} label={t("purchases.order.fields.supplier")}
error={!!errors.supplierId} error={!!errors.supplierId}
helperText={errors.supplierId?.message} helperText={errors.supplierId?.message}
/> />
@@ -311,7 +311,7 @@ export default function PurchaseOrderFormPage() {
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
label={t("purchases.orders.fields.destinationWarehouse")} label={t("purchases.order.fields.warehouse")}
/> />
)} )}
/> />
@@ -325,7 +325,7 @@ export default function PurchaseOrderFormPage() {
render={({ field }: { field: any }) => ( render={({ field }: { field: any }) => (
<TextField <TextField
{...field} {...field}
label={t("purchases.orders.fields.notes")} label={t("purchases.order.fields.notes")}
fullWidth fullWidth
disabled={isReadOnly} disabled={isReadOnly}
/> />
@@ -337,7 +337,7 @@ export default function PurchaseOrderFormPage() {
<Paper sx={{ p: 3 }}> <Paper sx={{ p: 3 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}> <Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
<Typography variant="h6">{t("purchases.orders.fields.lineTotal")}</Typography> <Typography variant="h6">{t("purchases.order.lines.total")}</Typography>
{!isReadOnly && ( {!isReadOnly && (
<Button startIcon={<AddIcon />} onClick={() => append({ <Button startIcon={<AddIcon />} onClick={() => append({
warehouseArticleId: 0, warehouseArticleId: 0,
@@ -356,12 +356,12 @@ export default function PurchaseOrderFormPage() {
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="30%">{t("purchases.orders.fields.article")}</TableCell> <TableCell width="30%">{t("purchases.order.lines.article")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.quantity")}</TableCell> <TableCell width="10%">{t("purchases.order.lines.quantity")}</TableCell>
<TableCell width="15%">{t("purchases.orders.fields.unitPrice")}</TableCell> <TableCell width="15%">{t("purchases.order.lines.price")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.discount")}</TableCell> <TableCell width="10%">{t("purchases.order.lines.discount")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.taxRate")}</TableCell> <TableCell width="10%">{t("purchases.order.lines.tax")}</TableCell>
<TableCell width="15%" align="right">{t("purchases.orders.fields.lineTotal")}</TableCell> <TableCell width="15%" align="right">{t("purchases.order.lines.total")}</TableCell>
<TableCell width="10%"></TableCell> <TableCell width="10%"></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -464,7 +464,7 @@ export default function PurchaseOrderFormPage() {
))} ))}
<TableRow> <TableRow>
<TableCell colSpan={5} align="right"> <TableCell colSpan={5} align="right">
<Typography fontWeight="bold">{t("purchases.orders.totals.gross")}</Typography> <Typography fontWeight="bold">{t("purchases.order.total")}</Typography>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography fontWeight="bold"> <Typography fontWeight="bold">

View File

@@ -56,15 +56,15 @@ export default function SuppliersPage() {
}; };
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ field: "code", headerName: t("purchases.suppliers.columns.code"), width: 120 }, { field: "code", headerName: t("purchases.supplier.columns.code"), width: 120 },
{ field: "name", headerName: t("purchases.suppliers.columns.name"), flex: 1, minWidth: 200 }, { field: "name", headerName: t("purchases.supplier.columns.name"), flex: 1, minWidth: 200 },
{ field: "vatNumber", headerName: t("purchases.suppliers.columns.vatNumber"), width: 150 }, { field: "vatNumber", headerName: t("purchases.supplier.columns.vatNumber"), width: 150 },
{ field: "email", headerName: t("purchases.suppliers.columns.email"), width: 200 }, { field: "email", headerName: t("purchases.supplier.columns.email"), width: 200 },
{ field: "phone", headerName: t("purchases.suppliers.columns.phone"), width: 150 }, { field: "phone", headerName: t("purchases.supplier.columns.phone"), width: 150 },
{ field: "city", headerName: t("purchases.suppliers.columns.city"), width: 150 }, { field: "city", headerName: t("purchases.supplier.columns.city"), width: 150 },
{ {
field: "isActive", field: "isActive",
headerName: t("purchases.suppliers.columns.status"), headerName: t("purchases.supplier.columns.status"),
width: 120, width: 120,
renderCell: (params: GridRenderCellParams<SupplierDto>) => ( renderCell: (params: GridRenderCellParams<SupplierDto>) => (
<Chip <Chip
@@ -110,13 +110,13 @@ export default function SuppliersPage() {
mb: 3, mb: 3,
}} }}
> >
<Typography variant="h4">{t("purchases.suppliers.title")}</Typography> <Typography variant="h4">{t("purchases.supplier.title")}</Typography>
<Button <Button
variant="contained" variant="contained"
startIcon={<AddIcon />} startIcon={<AddIcon />}
onClick={handleCreate} onClick={handleCreate}
> >
{t("purchases.suppliers.newSupplier")} {t("purchases.supplier.newSupplier")}
</Button> </Button>
</Box> </Box>

View File

@@ -45,6 +45,7 @@ import {
Save as SaveIcon, Save as SaveIcon,
Dataset as DatasetIcon, Dataset as DatasetIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
virtualDatasetService, virtualDatasetService,
@@ -91,6 +92,7 @@ export default function DatasetManagerDialog({
onClose, onClose,
onDatasetCreated, onDatasetCreated,
}: DatasetManagerDialogProps) { }: DatasetManagerDialogProps) {
const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// State // State
@@ -189,7 +191,7 @@ export default function DatasetManagerDialog({
} catch { } catch {
setValidationResult({ setValidationResult({
isValid: false, isValid: false,
errors: ["Errore durante la validazione"], errors: [t('reports.datasetManager.validationError')],
warnings: [], warnings: [],
}); });
return false; return false;
@@ -230,7 +232,7 @@ export default function DatasetManagerDialog({
}; };
const handleDeleteDataset = async (dataset: VirtualDatasetDto) => { const handleDeleteDataset = async (dataset: VirtualDatasetDto) => {
if (confirm(`Eliminare il dataset "${dataset.displayName}"?`)) { if (confirm(t('reports.datasetManager.deleteConfirm', { name: dataset.displayName }))) {
await deleteMutation.mutateAsync(dataset.id); await deleteMutation.mutateAsync(dataset.id);
} }
}; };
@@ -365,13 +367,13 @@ export default function DatasetManagerDialog({
alignItems: "center", alignItems: "center",
}} }}
> >
<Typography variant="h6">Dataset Virtuali</Typography> <Typography variant="h6">{t('reports.datasetManager.title')}</Typography>
<Button <Button
variant="contained" variant="contained"
startIcon={<AddIcon />} startIcon={<AddIcon />}
onClick={handleNewDataset} onClick={handleNewDataset}
> >
Nuovo Dataset {t('reports.datasetManager.newDataset')}
</Button> </Button>
</Box> </Box>
@@ -379,18 +381,17 @@ export default function DatasetManagerDialog({
<Box sx={{ p: 4, textAlign: "center" }}> <Box sx={{ p: 4, textAlign: "center" }}>
<DatasetIcon sx={{ fontSize: 64, color: "grey.400", mb: 2 }} /> <DatasetIcon sx={{ fontSize: 64, color: "grey.400", mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom> <Typography variant="h6" color="text.secondary" gutterBottom>
Nessun Dataset Virtuale {t('reports.datasetManager.noDatasets')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" mb={2}> <Typography variant="body2" color="text.secondary" mb={2}>
Crea dataset virtuali per combinare e filtrare i dati da più {t('reports.datasetManager.noDatasetsDesc')}
sorgenti.
</Typography> </Typography>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<AddIcon />} startIcon={<AddIcon />}
onClick={handleNewDataset} onClick={handleNewDataset}
> >
Crea il primo dataset {t('reports.datasetManager.createFirst')}
</Button> </Button>
</Box> </Box>
) : ( ) : (
@@ -400,17 +401,17 @@ export default function DatasetManagerDialog({
key={dataset.id} key={dataset.id}
secondaryAction={ secondaryAction={
<Box> <Box>
<Tooltip title="Modifica"> <Tooltip title={t('reports.toolbar.edit')}>
<IconButton onClick={() => handleEditDataset(dataset)}> <IconButton onClick={() => handleEditDataset(dataset)}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Duplica"> <Tooltip title={t('reports.toolbar.duplicate')}>
<IconButton onClick={() => handleCloneDataset(dataset)}> <IconButton onClick={() => handleCloneDataset(dataset)}>
<CopyIcon /> <CopyIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Elimina"> <Tooltip title={t('reports.toolbar.delete')}>
<IconButton <IconButton
onClick={() => handleDeleteDataset(dataset)} onClick={() => handleDeleteDataset(dataset)}
color="error" color="error"
@@ -428,7 +429,7 @@ export default function DatasetManagerDialog({
primary={dataset.displayName} primary={dataset.displayName}
secondary={ secondary={
<Box component="span"> <Box component="span">
{dataset.descrizione || "Nessuna descrizione"} {dataset.descrizione || t('reports.datasetManager.noDescription')}
<Box component="span" sx={{ display: "block", mt: 0.5 }}> <Box component="span" sx={{ display: "block", mt: 0.5 }}>
<Chip <Chip
label={dataset.categoria} label={dataset.categoria}
@@ -436,7 +437,7 @@ export default function DatasetManagerDialog({
sx={{ height: 20, fontSize: "0.7rem" }} sx={{ height: 20, fontSize: "0.7rem" }}
/> />
<Chip <Chip
label={`${dataset.configuration?.sources.length || 0} sorgenti`} label={`${dataset.configuration?.sources.length || 0} ${t('reports.datasetManager.sourcesCount')}`}
size="small" size="small"
sx={{ sx={{
ml: 0.5, ml: 0.5,
@@ -463,7 +464,7 @@ export default function DatasetManagerDialog({
{/* Header */} {/* Header */}
<Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}> <Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}>
<Typography variant="h6"> <Typography variant="h6">
{selectedDataset ? "Modifica Dataset" : "Nuovo Dataset Virtuale"} {selectedDataset ? t('reports.datasetManager.editDataset') : t('reports.datasetManager.newVirtualDataset')}
</Typography> </Typography>
</Box> </Box>
@@ -472,7 +473,7 @@ export default function DatasetManagerDialog({
<Box sx={{ px: 2, pt: 2 }}> <Box sx={{ px: 2, pt: 2 }}>
{validationResult.errors.length > 0 && ( {validationResult.errors.length > 0 && (
<Alert severity="error" sx={{ mb: 1 }}> <Alert severity="error" sx={{ mb: 1 }}>
<Typography variant="subtitle2">Errori:</Typography> <Typography variant="subtitle2">{t('reports.datasetManager.errors')}</Typography>
<ul style={{ margin: 0, paddingLeft: 20 }}> <ul style={{ margin: 0, paddingLeft: 20 }}>
{validationResult.errors.map((err, i) => ( {validationResult.errors.map((err, i) => (
<li key={i}>{err}</li> <li key={i}>{err}</li>
@@ -482,7 +483,7 @@ export default function DatasetManagerDialog({
)} )}
{validationResult.warnings.length > 0 && ( {validationResult.warnings.length > 0 && (
<Alert severity="warning"> <Alert severity="warning">
<Typography variant="subtitle2">Avvisi:</Typography> <Typography variant="subtitle2">{t('reports.datasetManager.warnings')}</Typography>
<ul style={{ margin: 0, paddingLeft: 20 }}> <ul style={{ margin: 0, paddingLeft: 20 }}>
{validationResult.warnings.map((warn, i) => ( {validationResult.warnings.map((warn, i) => (
<li key={i}>{warn}</li> <li key={i}>{warn}</li>
@@ -492,7 +493,7 @@ export default function DatasetManagerDialog({
)} )}
{validationResult.isValid && {validationResult.isValid &&
validationResult.warnings.length === 0 && ( validationResult.warnings.length === 0 && (
<Alert severity="success">Configurazione valida</Alert> <Alert severity="success">{t('reports.datasetManager.validConfig')}</Alert>
)} )}
</Box> </Box>
)} )}
@@ -503,24 +504,24 @@ export default function DatasetManagerDialog({
onChange={(_, v) => setActiveTab(v)} onChange={(_, v) => setActiveTab(v)}
sx={{ borderBottom: 1, borderColor: "divider" }} sx={{ borderBottom: 1, borderColor: "divider" }}
> >
<Tab label="Info" icon={<DatasetIcon />} iconPosition="start" /> <Tab label={t('reports.datasetManager.tabs.info')} icon={<DatasetIcon />} iconPosition="start" />
<Tab <Tab
label={`Sorgenti (${editingConfig.sources.length})`} label={`${t('reports.datasetManager.tabs.sources')} (${editingConfig.sources.length})`}
icon={<TableIcon />} icon={<TableIcon />}
iconPosition="start" iconPosition="start"
/> />
<Tab <Tab
label={`Relazioni (${editingConfig.relationships.length})`} label={`${t('reports.datasetManager.tabs.relationships')} (${editingConfig.relationships.length})`}
icon={<LinkIcon />} icon={<LinkIcon />}
iconPosition="start" iconPosition="start"
/> />
<Tab <Tab
label={`Filtri (${editingConfig.filters.filter((f) => f.enabled).length})`} label={`${t('reports.datasetManager.tabs.filters')} (${editingConfig.filters.filter((f) => f.enabled).length})`}
icon={<FilterIcon />} icon={<FilterIcon />}
iconPosition="start" iconPosition="start"
/> />
<Tab <Tab
label={`Campi (${editingConfig.outputFields.filter((f) => f.included).length})`} label={`${t('reports.datasetManager.tabs.fields')} (${editingConfig.outputFields.filter((f) => f.included).length})`}
icon={<FieldsIcon />} icon={<FieldsIcon />}
iconPosition="start" iconPosition="start"
/> />
@@ -536,8 +537,9 @@ export default function DatasetManagerDialog({
maxWidth: 500, maxWidth: 500,
}} }}
> >
<TextField <TextField
label="Nome Identificativo" label={t('reports.datasetManager.fields.nameId')}
value={editingInfo.nome} value={editingInfo.nome}
onChange={(e) => onChange={(e) =>
setEditingInfo((prev) => ({ setEditingInfo((prev) => ({
@@ -546,10 +548,10 @@ export default function DatasetManagerDialog({
})) }))
} }
required required
helperText="Nome univoco usato internamente (senza spazi)" helperText={t('reports.datasetManager.fields.nameIdHelper')}
/> />
<TextField <TextField
label="Nome Visualizzato" label={t('reports.datasetManager.fields.displayName')}
value={editingInfo.displayName} value={editingInfo.displayName}
onChange={(e) => onChange={(e) =>
setEditingInfo((prev) => ({ setEditingInfo((prev) => ({
@@ -560,7 +562,7 @@ export default function DatasetManagerDialog({
required required
/> />
<TextField <TextField
label="Descrizione" label={t('reports.datasetManager.fields.description')}
value={editingInfo.descrizione} value={editingInfo.descrizione}
onChange={(e) => onChange={(e) =>
setEditingInfo((prev) => ({ setEditingInfo((prev) => ({
@@ -572,10 +574,10 @@ export default function DatasetManagerDialog({
rows={2} rows={2}
/> />
<FormControl> <FormControl>
<InputLabel>Categoria</InputLabel> <InputLabel>{t('reports.datasetManager.fields.category')}</InputLabel>
<Select <Select
value={editingInfo.categoria} value={editingInfo.categoria}
label="Categoria" label={t('reports.datasetManager.fields.category')}
onChange={(e) => onChange={(e) =>
setEditingInfo((prev) => ({ setEditingInfo((prev) => ({
...prev, ...prev,
@@ -591,10 +593,10 @@ export default function DatasetManagerDialog({
</Select> </Select>
</FormControl> </FormControl>
<FormControl> <FormControl>
<InputLabel>Icona</InputLabel> <InputLabel>{t('reports.datasetManager.fields.icon')}</InputLabel>
<Select <Select
value={editingInfo.icon} value={editingInfo.icon}
label="Icona" label={t('reports.datasetManager.fields.icon')}
onChange={(e) => onChange={(e) =>
setEditingInfo((prev) => ({ ...prev, icon: e.target.value })) setEditingInfo((prev) => ({ ...prev, icon: e.target.value }))
} }
@@ -640,7 +642,7 @@ export default function DatasetManagerDialog({
{/* Dataset disponibili */} {/* Dataset disponibili */}
<Paper variant="outlined" sx={{ width: 280, p: 2 }}> <Paper variant="outlined" sx={{ width: 280, p: 2 }}>
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
Dataset Disponibili {t('reports.datasetManager.sources.available')}
</Typography> </Typography>
<Typography <Typography
variant="caption" variant="caption"
@@ -648,7 +650,7 @@ export default function DatasetManagerDialog({
display="block" display="block"
mb={2} mb={2}
> >
Clicca per aggiungere una sorgente {t('reports.datasetManager.sources.addInstruction')}
</Typography> </Typography>
<List dense> <List dense>
{availableBaseDatasets.map((dataset) => ( {availableBaseDatasets.map((dataset) => (
@@ -677,11 +679,11 @@ export default function DatasetManagerDialog({
{/* Sorgenti selezionate */} {/* Sorgenti selezionate */}
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Typography variant="subtitle2" gutterBottom> <Typography variant="subtitle2" gutterBottom>
Sorgenti nel Dataset ({editingConfig.sources.length}) {t('reports.datasetManager.sources.inDataset')} ({editingConfig.sources.length})
</Typography> </Typography>
{editingConfig.sources.length === 0 ? ( {editingConfig.sources.length === 0 ? (
<Alert severity="info"> <Alert severity="info">
Aggiungi almeno una sorgente dati dal pannello a sinistra {t('reports.datasetManager.sources.empty')}
</Alert> </Alert>
) : ( ) : (
<List> <List>
@@ -725,7 +727,7 @@ export default function DatasetManagerDialog({
</Box> </Box>
<TextField <TextField
label="Alias" label={t('reports.datasetManager.sources.alias')}
size="small" size="small"
value={source.alias} value={source.alias}
onChange={(e) => onChange={(e) =>
@@ -737,7 +739,7 @@ export default function DatasetManagerDialog({
{source.isPrimary ? ( {source.isPrimary ? (
<Chip <Chip
icon={<CheckIcon />} icon={<CheckIcon />}
label="Primario" label={t('reports.datasetManager.sources.primary')}
color="primary" color="primary"
size="small" size="small"
/> />
@@ -746,7 +748,7 @@ export default function DatasetManagerDialog({
size="small" size="small"
onClick={() => handleSetPrimary(source.id)} onClick={() => handleSetPrimary(source.id)}
> >
Imposta Primario {t('reports.datasetManager.sources.setPrimary')}
</Button> </Button>
)} )}

View File

@@ -66,6 +66,7 @@ import {
History as HistoryIcon, History as HistoryIcon,
AutoMode as AutoSaveIcon, AutoMode as AutoSaveIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import type { ElementType } from "../../../../types/report"; import type { ElementType } from "../../../../types/report";
// Snap options type // Snap options type
@@ -130,42 +131,42 @@ const ELEMENT_TYPES = [
{ {
type: "text" as ElementType, type: "text" as ElementType,
icon: TextIcon, icon: TextIcon,
label: "Testo", label: "reports.elements.text",
shortcut: "T", shortcut: "T",
color: "#2196f3", color: "#2196f3",
description: "Aggiungi un campo di testo", description: "reports.elements.textDesc",
}, },
{ {
type: "image" as ElementType, type: "image" as ElementType,
icon: ImageIcon, icon: ImageIcon,
label: "Immagine", label: "reports.elements.image",
shortcut: "I", shortcut: "I",
color: "#9c27b0", color: "#9c27b0",
description: "Inserisci un'immagine", description: "reports.elements.imageDesc",
}, },
{ {
type: "shape" as ElementType, type: "shape" as ElementType,
icon: ShapeIcon, icon: ShapeIcon,
label: "Forma", label: "reports.elements.shape",
shortcut: "R", shortcut: "R",
color: "#ff9800", color: "#ff9800",
description: "Disegna una forma geometrica", description: "reports.elements.shapeDesc",
}, },
{ {
type: "table" as ElementType, type: "table" as ElementType,
icon: TableIcon, icon: TableIcon,
label: "Tabella", label: "reports.elements.table",
shortcut: "B", shortcut: "B",
color: "#4caf50", color: "#4caf50",
description: "Inserisci una tabella dati", description: "reports.elements.tableDesc",
}, },
{ {
type: "line" as ElementType, type: "line" as ElementType,
icon: LineIcon, icon: LineIcon,
label: "Linea", label: "reports.elements.line",
shortcut: "L", shortcut: "L",
color: "#607d8b", color: "#607d8b",
description: "Traccia una linea", description: "reports.elements.lineDesc",
}, },
]; ];
@@ -174,47 +175,37 @@ const SNAP_OPTIONS_CONFIG = [
{ {
key: "grid" as keyof SnapOptions, key: "grid" as keyof SnapOptions,
icon: GridSnapIcon, icon: GridSnapIcon,
label: "Griglia", label: "reports.snap.grid",
description: "Allinea alla griglia", description: "reports.snap.grid",
}, },
{ {
key: "objects" as keyof SnapOptions, key: "objects" as keyof SnapOptions,
icon: ObjectSnapIcon, icon: ObjectSnapIcon,
label: "Oggetti", label: "reports.snap.objects",
description: "Allinea agli altri oggetti", description: "reports.snap.objects",
}, },
{ {
key: "borders" as keyof SnapOptions, key: "borders" as keyof SnapOptions,
icon: BorderSnapIcon, icon: BorderSnapIcon,
label: "Margini", label: "reports.snap.borders",
description: "Allinea ai margini pagina", description: "reports.snap.borders",
}, },
{ {
key: "center" as keyof SnapOptions, key: "center" as keyof SnapOptions,
icon: CenterSnapIcon, icon: CenterSnapIcon,
label: "Centro", label: "reports.snap.center",
description: "Allinea al centro", description: "reports.snap.center",
}, },
{ {
key: "tangent" as keyof SnapOptions, key: "tangent" as keyof SnapOptions,
icon: TangentSnapIcon, icon: TangentSnapIcon,
label: "Bordi", label: "reports.snap.tangent",
description: "Allinea ai bordi adiacenti", description: "reports.snap.tangent",
}, },
]; ];
// Format time ago // Format time ago
function formatTimeAgo(date: Date | null | undefined): string {
if (!date) return "";
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "Ora";
if (minutes < 60) return `${minutes}m fa`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h fa`;
return date.toLocaleDateString("it-IT", { day: "2-digit", month: "short" });
}
// ToolbarSection component for consistent styling // ToolbarSection component for consistent styling
function ToolbarSection({ function ToolbarSection({
@@ -353,7 +344,20 @@ export default function EditorToolbar({
autoSaveEnabled = true, autoSaveEnabled = true,
onAutoSaveToggle, onAutoSaveToggle,
}: EditorToolbarProps) { }: EditorToolbarProps) {
const { t, i18n } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const formatTimeAgo = (date: Date | null | undefined) => {
if (!date) return "";
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return t('reports.time.now');
if (minutes < 60) return t('reports.time.minutesAgo', { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t('reports.time.hoursAgo', { count: hours });
return date.toLocaleDateString(i18n.language, { day: "2-digit", month: "short" });
};
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md")); const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md"));
const isSmallScreen = useMediaQuery(theme.breakpoints.down("lg")); const isSmallScreen = useMediaQuery(theme.breakpoints.down("lg"));
@@ -412,8 +416,8 @@ export default function EditorToolbar({
<Tooltip <Tooltip
title={ title={
autoSaveEnabled autoSaveEnabled
? "Auto-salvataggio attivo" ? t('reports.toolbar.autoSaveOn')
: "Auto-salvataggio disattivato" : t('reports.toolbar.autoSaveOff')
} }
> >
<IconButton <IconButton
@@ -439,11 +443,11 @@ export default function EditorToolbar({
{/* Save status */} {/* Save status */}
{isSaving ? ( {isSaving ? (
<Tooltip title="Salvataggio in corso..."> <Tooltip title={t('reports.toolbar.saving')}>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<CircularProgress size={16} /> <CircularProgress size={16} />
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
Salvo... {t('reports.toolbar.saving')}
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
@@ -451,14 +455,14 @@ export default function EditorToolbar({
<Tooltip <Tooltip
title={ title={
autoSaveEnabled autoSaveEnabled
? "Salvataggio automatico in attesa..." ? t('reports.toolbar.autoSavePending')
: "Modifiche non salvate" : t('reports.toolbar.unsavedTooltip')
} }
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<UnsavedIcon fontSize="small" sx={{ color: "warning.main" }} /> <UnsavedIcon fontSize="small" sx={{ color: "warning.main" }} />
<Typography variant="caption" color="warning.main"> <Typography variant="caption" color="warning.main">
Non salvato {t('reports.toolbar.unsaved')}
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
@@ -466,14 +470,14 @@ export default function EditorToolbar({
<Tooltip <Tooltip
title={ title={
lastSavedAt lastSavedAt
? `Ultimo salvataggio: ${formatTimeAgo(lastSavedAt)}` ? `${t('reports.toolbar.saved')}: ${formatTimeAgo(lastSavedAt)}`
: "Salvato" : t('reports.toolbar.saved')
} }
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<SavedIcon fontSize="small" sx={{ color: "success.main" }} /> <SavedIcon fontSize="small" sx={{ color: "success.main" }} />
<Typography variant="caption" color="success.main"> <Typography variant="caption" color="success.main">
{formatTimeAgo(lastSavedAt) || "Salvato"} {formatTimeAgo(lastSavedAt) || t('reports.toolbar.saved')}
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
@@ -523,14 +527,14 @@ export default function EditorToolbar({
{/* Undo/Redo */} {/* Undo/Redo */}
<StyledIconButton <StyledIconButton
tooltip="Annulla" tooltip={t('reports.toolbar.undo')}
onClick={onUndo} onClick={onUndo}
disabled={!canUndo} disabled={!canUndo}
> >
<UndoIcon fontSize="small" /> <UndoIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Ripeti" tooltip={t('reports.toolbar.redo')}
onClick={onRedo} onClick={onRedo}
disabled={!canRedo} disabled={!canRedo}
> >
@@ -541,7 +545,7 @@ export default function EditorToolbar({
{/* Delete */} {/* Delete */}
<StyledIconButton <StyledIconButton
tooltip="Elimina" tooltip={t('reports.toolbar.delete')}
onClick={onDeleteElement} onClick={onDeleteElement}
disabled={!hasSelection} disabled={!hasSelection}
color="#f44336" color="#f44336"
@@ -571,12 +575,12 @@ export default function EditorToolbar({
<Divider orientation="vertical" flexItem sx={{ mx: 0.25 }} /> <Divider orientation="vertical" flexItem sx={{ mx: 0.25 }} />
{/* Save/Preview */} {/* Save/Preview */}
<StyledIconButton tooltip="Anteprima" onClick={onPreview}> <StyledIconButton tooltip={t('reports.toolbar.preview')} onClick={onPreview}>
<PreviewIcon fontSize="small" /> <PreviewIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
{!autoSaveEnabled && ( {!autoSaveEnabled && (
<StyledIconButton <StyledIconButton
tooltip="Salva" tooltip={t('reports.toolbar.save')}
onClick={onSave} onClick={onSave}
disabled={isSaving} disabled={isSaving}
color="#1976d2" color="#1976d2"
@@ -615,7 +619,7 @@ export default function EditorToolbar({
> >
{/* Zoom */} {/* Zoom */}
<StyledIconButton <StyledIconButton
tooltip="Zoom out" tooltip={t('reports.toolbar.zoomOut')}
onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))} onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))}
> >
<ZoomOutIcon fontSize="small" /> <ZoomOutIcon fontSize="small" />
@@ -634,7 +638,7 @@ export default function EditorToolbar({
{Math.round(zoom * 100)}% {Math.round(zoom * 100)}%
</Button> </Button>
<StyledIconButton <StyledIconButton
tooltip="Zoom in" tooltip={t('reports.toolbar.zoomIn')}
onClick={() => onZoomChange(Math.min(3, zoom + 0.25))} onClick={() => onZoomChange(Math.min(3, zoom + 0.25))}
> >
<ZoomInIcon fontSize="small" /> <ZoomInIcon fontSize="small" />
@@ -644,7 +648,7 @@ export default function EditorToolbar({
{/* Grid & Snap */} {/* Grid & Snap */}
<StyledIconButton <StyledIconButton
tooltip="Griglia" tooltip={t('reports.snap.grid')}
onClick={onToggleGrid} onClick={onToggleGrid}
active={showGrid} active={showGrid}
> >
@@ -655,7 +659,7 @@ export default function EditorToolbar({
)} )}
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Snap" tooltip={t('reports.snap.options')}
onClick={(e) => setSnapMenuAnchor(e.currentTarget)} onClick={(e) => setSnapMenuAnchor(e.currentTarget)}
active={activeSnapCount > 0} active={activeSnapCount > 0}
badge={activeSnapCount || undefined} badge={activeSnapCount || undefined}
@@ -667,14 +671,14 @@ export default function EditorToolbar({
{/* Copy/Lock */} {/* Copy/Lock */}
<StyledIconButton <StyledIconButton
tooltip="Duplica" tooltip={t('reports.toolbar.duplicate')}
onClick={onCopyElement} onClick={onCopyElement}
disabled={!hasSelection} disabled={!hasSelection}
> >
<CopyIcon fontSize="small" /> <CopyIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip={isLocked ? "Sblocca" : "Blocca"} tooltip={isLocked ? t('reports.toolbar.unlock') : t('reports.toolbar.lock')}
onClick={onToggleLock} onClick={onToggleLock}
disabled={!hasSelection} disabled={!hasSelection}
active={isLocked} active={isLocked}
@@ -711,8 +715,8 @@ export default function EditorToolbar({
</Avatar> </Avatar>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={label} primary={t(label)}
secondary={description} secondary={t(description)}
primaryTypographyProps={{ fontWeight: 500 }} primaryTypographyProps={{ fontWeight: 500 }}
secondaryTypographyProps={{ fontSize: "0.7rem" }} secondaryTypographyProps={{ fontSize: "0.7rem" }}
/> />
@@ -775,7 +779,7 @@ export default function EditorToolbar({
mb={1} mb={1}
> >
<Typography variant="subtitle2" fontWeight={600}> <Typography variant="subtitle2" fontWeight={600}>
Opzioni Snap {t('reports.snap.options')}
</Typography> </Typography>
<Switch <Switch
size="small" size="small"
@@ -798,27 +802,23 @@ export default function EditorToolbar({
}} }}
> >
<ListItemIcon sx={{ minWidth: 32 }}> <ListItemIcon sx={{ minWidth: 32 }}>
<Icon <Icon fontSize="small" color={snapOptions[key] ? "primary" : "inherit"} />
fontSize="small"
color={snapOptions[key] ? "primary" : "inherit"}
/>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={t(label)} />
primary={label}
primaryTypographyProps={{ variant: "body2" }}
/>
<Switch size="small" checked={snapOptions[key]} />
</ListItemButton> </ListItemButton>
))} ))}
</Box> </Box>
</Popover> </Popover >
{/* Zoom Popover */} {/* Zoom Popover */}
<Popover < Popover
open={Boolean(zoomMenuAnchor)} open={Boolean(zoomMenuAnchor)}
anchorEl={zoomMenuAnchor} anchorEl={zoomMenuAnchor}
onClose={() => setZoomMenuAnchor(null)} onClose={() => setZoomMenuAnchor(null)
anchorOrigin={{ vertical: "bottom", horizontal: "center" }} }
anchorOrigin={{ vertical: "bottom", horizontal: "center" }
}
PaperProps={{ sx: { borderRadius: 2 } }} PaperProps={{ sx: { borderRadius: 2 } }}
> >
<Box sx={{ p: 1.5, width: 180 }}> <Box sx={{ p: 1.5, width: 180 }}>
@@ -850,8 +850,8 @@ export default function EditorToolbar({
))} ))}
</Box> </Box>
</Box> </Box>
</Popover> </Popover >
</Paper> </Paper >
); );
} }
@@ -883,7 +883,7 @@ export default function EditorToolbar({
onClick={(e) => setAddMenuAnchor(e.currentTarget)} onClick={(e) => setAddMenuAnchor(e.currentTarget)}
sx={{ borderRadius: 2, textTransform: "none", fontWeight: 600 }} sx={{ borderRadius: 2, textTransform: "none", fontWeight: 600 }}
> >
Aggiungi {t('reports.elements.add')}
</Button> </Button>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} /> <Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
@@ -906,14 +906,14 @@ export default function EditorToolbar({
{/* Selection Actions */} {/* Selection Actions */}
<StyledIconButton <StyledIconButton
tooltip="Duplica" tooltip={t('reports.toolbar.duplicate')}
onClick={onCopyElement} onClick={onCopyElement}
disabled={!hasSelection} disabled={!hasSelection}
> >
<CopyIcon fontSize="small" /> <CopyIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Elimina" tooltip={t('reports.toolbar.delete')}
onClick={onDeleteElement} onClick={onDeleteElement}
disabled={!hasSelection} disabled={!hasSelection}
color="#f44336" color="#f44336"
@@ -921,7 +921,7 @@ export default function EditorToolbar({
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip={isLocked ? "Sblocca" : "Blocca"} tooltip={isLocked ? t('reports.toolbar.unlock') : t('reports.toolbar.lock')}
onClick={onToggleLock} onClick={onToggleLock}
disabled={!hasSelection} disabled={!hasSelection}
active={isLocked} active={isLocked}
@@ -938,14 +938,14 @@ export default function EditorToolbar({
{/* Undo/Redo */} {/* Undo/Redo */}
<StyledIconButton <StyledIconButton
tooltip="Annulla (Ctrl+Z)" tooltip={`${t('reports.toolbar.undo')} (Ctrl+Z)`}
onClick={onUndo} onClick={onUndo}
disabled={!canUndo} disabled={!canUndo}
> >
<UndoIcon fontSize="small" /> <UndoIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Ripeti (Ctrl+Y)" tooltip={`${t('reports.toolbar.redo')} (Ctrl+Y)`}
onClick={onRedo} onClick={onRedo}
disabled={!canRedo} disabled={!canRedo}
> >
@@ -956,7 +956,7 @@ export default function EditorToolbar({
{/* View Controls */} {/* View Controls */}
<StyledIconButton <StyledIconButton
tooltip="Griglia" tooltip={t('reports.snap.grid')}
onClick={onToggleGrid} onClick={onToggleGrid}
active={showGrid} active={showGrid}
> >
@@ -967,7 +967,7 @@ export default function EditorToolbar({
)} )}
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Snap" tooltip={t('reports.snap.options')}
onClick={(e) => setSnapMenuAnchor(e.currentTarget)} onClick={(e) => setSnapMenuAnchor(e.currentTarget)}
active={activeSnapCount > 0} active={activeSnapCount > 0}
badge={activeSnapCount || undefined} badge={activeSnapCount || undefined}
@@ -979,7 +979,7 @@ export default function EditorToolbar({
{/* Zoom */} {/* Zoom */}
<StyledIconButton <StyledIconButton
tooltip="Zoom out" tooltip={t('reports.toolbar.zoomOut')}
onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))} onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))}
> >
<ZoomOutIcon fontSize="small" /> <ZoomOutIcon fontSize="small" />
@@ -998,7 +998,7 @@ export default function EditorToolbar({
{Math.round(zoom * 100)}% {Math.round(zoom * 100)}%
</Button> </Button>
<StyledIconButton <StyledIconButton
tooltip="Zoom in" tooltip={t('reports.toolbar.zoomIn')}
onClick={() => onZoomChange(Math.min(3, zoom + 0.25))} onClick={() => onZoomChange(Math.min(3, zoom + 0.25))}
> >
<ZoomInIcon fontSize="small" /> <ZoomInIcon fontSize="small" />
@@ -1008,7 +1008,7 @@ export default function EditorToolbar({
{/* Page Navigation */} {/* Page Navigation */}
<StyledIconButton <StyledIconButton
tooltip="Pagina precedente" tooltip={t('reports.toolbar.prevPage')}
onClick={onPrevPage} onClick={onPrevPage}
disabled={currentPageIndex <= 0} disabled={currentPageIndex <= 0}
> >
@@ -1034,7 +1034,7 @@ export default function EditorToolbar({
</Button> </Button>
</Tooltip> </Tooltip>
<StyledIconButton <StyledIconButton
tooltip="Pagina successiva" tooltip={t('reports.toolbar.nextPage')}
onClick={onNextPage} onClick={onNextPage}
disabled={currentPageIndex >= totalPages - 1} disabled={currentPageIndex >= totalPages - 1}
> >
@@ -1051,7 +1051,7 @@ export default function EditorToolbar({
onClick={onPreview} onClick={onPreview}
sx={{ borderRadius: 2, textTransform: "none" }} sx={{ borderRadius: 2, textTransform: "none" }}
> >
Anteprima {t('reports.toolbar.preview')}
</Button> </Button>
{!autoSaveEnabled && ( {!autoSaveEnabled && (
<Button <Button
@@ -1068,7 +1068,7 @@ export default function EditorToolbar({
disabled={isSaving} disabled={isSaving}
sx={{ borderRadius: 2, textTransform: "none", fontWeight: 600 }} sx={{ borderRadius: 2, textTransform: "none", fontWeight: 600 }}
> >
{isSaving ? "Salvo..." : "Salva"} {isSaving ? t('reports.toolbar.saving') : t('reports.toolbar.save')}
</Button> </Button>
)} )}
</Box> </Box>
@@ -1317,7 +1317,7 @@ export default function EditorToolbar({
color: "text.secondary", color: "text.secondary",
}} }}
> >
Inserisci elemento {t('reports.elements.insert')}
</Typography> </Typography>
<List dense sx={{ py: 0 }}> <List dense sx={{ py: 0 }}>
{ELEMENT_TYPES.map( {ELEMENT_TYPES.map(
@@ -1388,16 +1388,16 @@ export default function EditorToolbar({
{/* Selection Actions */} {/* Selection Actions */}
<ToolbarSection label={isSmallScreen ? undefined : "MODIFICA"}> <ToolbarSection label={isSmallScreen ? undefined : t('reports.toolbar.edit')}>
<StyledIconButton <StyledIconButton
tooltip="Duplica (Ctrl+D)" tooltip={`${t('reports.toolbar.duplicate')} (Ctrl+D)`}
onClick={onCopyElement} onClick={onCopyElement}
disabled={!hasSelection} disabled={!hasSelection}
> >
<CopyIcon fontSize="small" /> <CopyIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Elimina (Canc)" tooltip={`${t('reports.toolbar.delete')} (Canc)`}
onClick={onDeleteElement} onClick={onDeleteElement}
disabled={!hasSelection} disabled={!hasSelection}
color="#f44336" color="#f44336"
@@ -1405,7 +1405,7 @@ export default function EditorToolbar({
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip={isLocked ? "Sblocca (Ctrl+L)" : "Blocca (Ctrl+L)"} tooltip={isLocked ? `${t('reports.toolbar.unlock')} (Ctrl+L)` : `${t('reports.toolbar.lock')} (Ctrl+L)`}
onClick={onToggleLock} onClick={onToggleLock}
disabled={!hasSelection} disabled={!hasSelection}
active={isLocked} active={isLocked}
@@ -1426,16 +1426,16 @@ export default function EditorToolbar({
/> />
{/* History */} {/* History */}
<ToolbarSection label={isSmallScreen ? undefined : "CRONOLOGIA"}> <ToolbarSection label={isSmallScreen ? undefined : t('reports.toolbar.history')}>
<StyledIconButton <StyledIconButton
tooltip="Annulla (Ctrl+Z)" tooltip={`${t('reports.toolbar.undo')} (Ctrl+Z)`}
onClick={onUndo} onClick={onUndo}
disabled={!canUndo} disabled={!canUndo}
> >
<UndoIcon fontSize="small" /> <UndoIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Ripeti (Ctrl+Y)" tooltip={`${t('reports.toolbar.redo')} (Ctrl+Y)`}
onClick={onRedo} onClick={onRedo}
disabled={!canRedo} disabled={!canRedo}
> >
@@ -1443,7 +1443,7 @@ export default function EditorToolbar({
</StyledIconButton> </StyledIconButton>
{onOpenHistory && ( {onOpenHistory && (
<StyledIconButton <StyledIconButton
tooltip="Cronologia modifiche" tooltip={t('reports.toolbar.historyTooltip')}
onClick={onOpenHistory} onClick={onOpenHistory}
> >
<HistoryIcon fontSize="small" /> <HistoryIcon fontSize="small" />
@@ -1458,9 +1458,9 @@ export default function EditorToolbar({
/> />
{/* View Controls */} {/* View Controls */}
<ToolbarSection label={isSmallScreen ? undefined : "VISTA"}> <ToolbarSection label={isSmallScreen ? undefined : t('reports.toolbar.view')}>
<StyledIconButton <StyledIconButton
tooltip={showGrid ? "Nascondi griglia (G)" : "Mostra griglia (G)"} tooltip={showGrid ? `${t('reports.snap.hideGrid')} (G)` : `${t('reports.snap.showGrid')} (G)`}
onClick={onToggleGrid} onClick={onToggleGrid}
active={showGrid} active={showGrid}
> >
@@ -1498,7 +1498,7 @@ export default function EditorToolbar({
}, },
}} }}
> >
Snap {t('reports.snap.options')}
</Button> </Button>
</ToolbarSection> </ToolbarSection>
@@ -1518,7 +1518,7 @@ export default function EditorToolbar({
mb={1.5} mb={1.5}
> >
<Typography variant="subtitle1" fontWeight={600}> <Typography variant="subtitle1" fontWeight={600}>
Allineamento automatico {t('reports.snap.autoAlign')}
</Typography> </Typography>
<FormControlLabel <FormControlLabel
control={ control={
@@ -1529,7 +1529,7 @@ export default function EditorToolbar({
} }
label={ label={
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Tutti {t('reports.snap.all')}
</Typography> </Typography>
} }
labelPlacement="start" labelPlacement="start"
@@ -1606,9 +1606,9 @@ export default function EditorToolbar({
/> />
{/* Zoom Controls */} {/* Zoom Controls */}
<ToolbarSection label={isSmallScreen ? undefined : "ZOOM"}> <ToolbarSection label={isSmallScreen ? undefined : t('reports.toolbar.zoom')}>
<StyledIconButton <StyledIconButton
tooltip="Riduci zoom (-)" tooltip={`${t('reports.toolbar.zoomOut')} (-)`}
onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))} onClick={() => onZoomChange(Math.max(0.25, zoom - 0.25))}
> >
<ZoomOutIcon fontSize="small" /> <ZoomOutIcon fontSize="small" />
@@ -1632,14 +1632,14 @@ export default function EditorToolbar({
</Button> </Button>
<StyledIconButton <StyledIconButton
tooltip="Aumenta zoom (+)" tooltip={`${t('reports.toolbar.zoomIn')} (+)`}
onClick={() => onZoomChange(Math.min(3, zoom + 0.25))} onClick={() => onZoomChange(Math.min(3, zoom + 0.25))}
> >
<ZoomInIcon fontSize="small" /> <ZoomInIcon fontSize="small" />
</StyledIconButton> </StyledIconButton>
<StyledIconButton <StyledIconButton
tooltip="Adatta alla finestra" tooltip={t('reports.toolbar.fitWindow')}
onClick={() => onZoomChange(0.75)} onClick={() => onZoomChange(0.75)}
> >
<FitIcon fontSize="small" /> <FitIcon fontSize="small" />
@@ -1660,7 +1660,7 @@ export default function EditorToolbar({
gutterBottom gutterBottom
sx={{ display: "block" }} sx={{ display: "block" }}
> >
Livello zoom: {Math.round(zoom * 100)}% {t('reports.toolbar.zoomLevel')}: {Math.round(zoom * 100)}%
</Typography> </Typography>
<Slider <Slider
value={zoom} value={zoom}
@@ -1677,7 +1677,7 @@ export default function EditorToolbar({
color="text.secondary" color="text.secondary"
sx={{ mb: 1, display: "block" }} sx={{ mb: 1, display: "block" }}
> >
Preset {t('reports.toolbar.presets')}
</Typography> </Typography>
<Box display="flex" flexWrap="wrap" gap={0.5}> <Box display="flex" flexWrap="wrap" gap={0.5}>
{ZOOM_PRESETS.map(({ value, label }) => ( {ZOOM_PRESETS.map(({ value, label }) => (
@@ -1711,7 +1711,7 @@ export default function EditorToolbar({
{/* Command Palette / Search */} {/* Command Palette / Search */}
{onOpenCommandPalette && ( {onOpenCommandPalette && (
<Tooltip title="Cerca comando (Ctrl+K)"> <Tooltip title={`${t('reports.toolbar.searchCommand')} (Ctrl+K)`}>
<Button <Button
size="small" size="small"
onClick={onOpenCommandPalette} onClick={onOpenCommandPalette}
@@ -1725,14 +1725,14 @@ export default function EditorToolbar({
"&:hover": { bgcolor: "action.selected" }, "&:hover": { bgcolor: "action.selected" },
}} }}
> >
Cerca... {t('reports.toolbar.searchCommand')}...
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
{/* Keyboard Shortcuts */} {/* Keyboard Shortcuts */}
<StyledIconButton <StyledIconButton
tooltip="Scorciatoie tastiera" tooltip={t('reports.toolbar.shortcuts')}
onClick={(e) => setShortcutsAnchor(e.currentTarget)} onClick={(e) => setShortcutsAnchor(e.currentTarget)}
> >
<ShortcutsIcon fontSize="small" /> <ShortcutsIcon fontSize="small" />
@@ -1748,7 +1748,7 @@ export default function EditorToolbar({
PaperProps={{ sx: { mt: 1, borderRadius: 2, p: 2, minWidth: 300 } }} PaperProps={{ sx: { mt: 1, borderRadius: 2, p: 2, minWidth: 300 } }}
> >
<Typography variant="subtitle1" fontWeight={600} gutterBottom> <Typography variant="subtitle1" fontWeight={600} gutterBottom>
Scorciatoie Tastiera {t('reports.toolbar.shortcutsTitle')}
</Typography> </Typography>
<Divider sx={{ mb: 1.5 }} /> <Divider sx={{ mb: 1.5 }} />
<Box <Box
@@ -1761,17 +1761,17 @@ export default function EditorToolbar({
> >
<tbody> <tbody>
{[ {[
["Ctrl + Z", "Annulla"], ["Ctrl + Z", t('reports.toolbar.undo')],
["Ctrl + Y", "Ripeti"], ["Ctrl + Y", t('reports.toolbar.redo')],
["Ctrl + S", "Salva"], ["Ctrl + S", t('reports.toolbar.save')],
["Ctrl + D", "Duplica"], ["Ctrl + D", t('reports.toolbar.duplicate')],
["Ctrl + K", "Cerca comando"], ["Ctrl + K", t('reports.toolbar.searchCommand')],
["Canc / Backspace", "Elimina"], ["Canc / Backspace", t('reports.toolbar.delete')],
["Frecce", "Sposta (1px)"], ["Frecce", t('reports.shortcuts.move1px')],
["Shift + Frecce", "Sposta (10px)"], ["Shift + Frecce", t('reports.shortcuts.move10px')],
["G", "Mostra/nascondi griglia"], ["G", t('reports.shortcuts.toggleGrid')],
["+ / -", "Zoom in/out"], ["+ / -", t('reports.shortcuts.zoomInOut')],
["PgUp / PgDn", "Cambia pagina"], ["PgUp / PgDn", t('reports.shortcuts.changePage')],
].map(([key, action]) => ( ].map(([key, action]) => (
<tr key={key}> <tr key={key}>
<td> <td>

View File

@@ -43,6 +43,7 @@ import {
Close as CloseIcon, Close as CloseIcon,
ArrowBack as BackIcon, ArrowBack as BackIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useQueries } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query";
import { reportGeneratorService } from "../../../../services/reportService"; import { reportGeneratorService } from "../../../../services/reportService";
import type { import type {
@@ -66,6 +67,7 @@ export default function PreviewDialog({
onGeneratePreview, onGeneratePreview,
isGenerating, isGenerating,
}: PreviewDialogProps) { }: PreviewDialogProps) {
const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@@ -268,7 +270,7 @@ export default function PreviewDialog({
<ListItemText <ListItemText
primary={dataset.name} primary={dataset.name}
secondary={ secondary={
selectedEntity ? selectedEntity.label : "Non selezionato" selectedEntity ? selectedEntity.label : t('reports.preview.notSelected')
} }
primaryTypographyProps={{ primaryTypographyProps={{
variant: "body2", variant: "body2",
@@ -281,7 +283,7 @@ export default function PreviewDialog({
}} }}
/> />
{isSelected && ( {isSelected && (
<Tooltip title="Rimuovi selezione"> <Tooltip title={t('reports.preview.removeSelection')}>
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={(e) => {
@@ -327,7 +329,7 @@ export default function PreviewDialog({
<IconButton size="small" onClick={() => setMobileShowList(true)}> <IconButton size="small" onClick={() => setMobileShowList(true)}>
<BackIcon /> <BackIcon />
</IconButton> </IconButton>
<Typography variant="subtitle2">Seleziona</Typography> <Typography variant="subtitle2">{t('reports.preview.select')}</Typography>
</Box> </Box>
)} )}
<Box display="flex" alignItems="center" gap={1} mb={1}> <Box display="flex" alignItems="center" gap={1} mb={1}>
@@ -357,7 +359,7 @@ export default function PreviewDialog({
{/* Ricerca */} {/* Ricerca */}
<TextField <TextField
placeholder={`Cerca...`} placeholder={t('reports.preview.searchPlaceholder')}
size="small" size="small"
fullWidth fullWidth
value={searchTerms[activeDataset || ""] || ""} value={searchTerms[activeDataset || ""] || ""}
@@ -392,8 +394,8 @@ export default function PreviewDialog({
<Box sx={{ p: 3, textAlign: "center" }}> <Box sx={{ p: 3, textAlign: "center" }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{searchTerms[activeDataset || ""] {searchTerms[activeDataset || ""]
? "Nessun risultato trovato" ? t('reports.preview.noResults')
: "Nessuna entità disponibile"} : t('reports.preview.noEntities')}
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
@@ -482,7 +484,7 @@ export default function PreviewDialog({
}} }}
> >
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{filteredEntities.length} risultati {filteredEntities.length} {t('reports.preview.results')}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -511,7 +513,7 @@ export default function PreviewDialog({
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<Typography variant="h6" sx={{ flex: 1, ml: 1 }}> <Typography variant="h6" sx={{ flex: 1, ml: 1 }}>
Anteprima Report {t('reports.preview.title')}
</Typography> </Typography>
<Chip <Chip
label={`${selectedCount}/${selectedDatasets.length}`} label={`${selectedCount}/${selectedDatasets.length}`}
@@ -527,15 +529,15 @@ export default function PreviewDialog({
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
> >
<Typography variant="h6">Anteprima Report</Typography> <Typography variant="h6">{t('reports.preview.title')}</Typography>
<Chip <Chip
label={`${selectedCount}/${selectedDatasets.length} selezionati`} label={`${selectedCount}/${selectedDatasets.length} ${t('reports.preview.selected')}`}
color={allSelected ? "success" : "default"} color={allSelected ? "success" : "default"}
size="small" size="small"
/> />
</Box> </Box>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Seleziona un'entità per ogni dataset da utilizzare nell'anteprima {t('reports.preview.instruction')}
</Typography> </Typography>
</DialogTitle> </DialogTitle>
)} )}
@@ -545,15 +547,14 @@ export default function PreviewDialog({
<DialogContent sx={{ p: 0, display: "flex", overflow: "hidden" }}> <DialogContent sx={{ p: 0, display: "flex", overflow: "hidden" }}>
{hasError && ( {hasError && (
<Alert severity="error" sx={{ m: 2 }}> <Alert severity="error" sx={{ m: 2 }}>
Errore nel caricamento dei dati disponibili {t('reports.preview.errorLoading')}
</Alert> </Alert>
)} )}
{selectedDatasets.length === 0 ? ( {selectedDatasets.length === 0 ? (
<Box sx={{ p: 3, textAlign: "center", width: "100%" }}> <Box sx={{ p: 3, textAlign: "center", width: "100%" }}>
<Alert severity="info"> <Alert severity="info">
Non ci sono dataset selezionati per questo template. Aggiungi {t('reports.preview.noDatasets')}
almeno un dataset per poter generare l'anteprima.
</Alert> </Alert>
</Box> </Box>
) : isMobile ? ( ) : isMobile ? (
@@ -577,7 +578,7 @@ export default function PreviewDialog({
}} }}
> >
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Seleziona un'entità per ogni dataset {t('reports.preview.selectEntityInstruction')}
</Typography> </Typography>
</Box> </Box>
{renderDatasetList()} {renderDatasetList()}
@@ -613,7 +614,7 @@ export default function PreviewDialog({
<DialogActions sx={{ px: isMobile ? 2 : 3, py: 2 }}> <DialogActions sx={{ px: isMobile ? 2 : 3, py: 2 }}>
<Button onClick={onClose} fullWidth={isMobile}> <Button onClick={onClose} fullWidth={isMobile}>
Annulla {t('reports.preview.cancel')}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@@ -625,10 +626,10 @@ export default function PreviewDialog({
fullWidth={isMobile} fullWidth={isMobile}
> >
{isGenerating {isGenerating
? "Generazione..." ? t('reports.preview.generating')
: isMobile : isMobile
? "Genera PDF" ? t('reports.preview.generatePdf')
: "Genera Anteprima PDF"} : t('reports.preview.generatePreviewPdf')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -51,6 +51,7 @@ interface SearchOption {
label: string; label: string;
path: string; path: string;
category: string; category: string;
translationKey?: string;
} }
export default function SearchBar() { export default function SearchBar() {
@@ -63,55 +64,55 @@ export default function SearchBar() {
const options = useMemo(() => { const options = useMemo(() => {
const opts: SearchOption[] = [ const opts: SearchOption[] = [
// Core // Core
{ label: t('menu.dashboard'), path: '/', category: 'Zentral' }, { label: t('menu.dashboard'), path: '/', category: 'Zentral', translationKey: 'menu.dashboard' },
{ label: t('menu.calendar'), path: '/calendario', category: 'Zentral' }, { label: t('menu.calendar'), path: '/calendario', category: 'Zentral', translationKey: 'menu.calendar' },
{ label: t('menu.events'), path: '/eventi', category: 'Zentral' }, { label: t('menu.events'), path: '/eventi', category: 'Zentral', translationKey: 'menu.events' },
{ label: t('menu.clients'), path: '/clienti', category: 'Zentral' }, { label: t('menu.clients'), path: '/clienti', category: 'Zentral', translationKey: 'menu.clients' },
{ label: t('menu.location'), path: '/location', category: 'Zentral' }, { label: t('menu.location'), path: '/location', category: 'Zentral', translationKey: 'menu.location' },
{ label: t('menu.articles'), path: '/articoli', category: 'Zentral' }, { label: t('menu.articles'), path: '/articoli', category: 'Zentral', translationKey: 'menu.articles' },
{ label: t('menu.resources'), path: '/risorse', category: 'Zentral' }, { label: t('menu.resources'), path: '/risorse', category: 'Zentral', translationKey: 'menu.resources' },
{ label: t('menu.reports'), path: '/report-templates', category: 'Zentral' }, { label: t('menu.reports'), path: '/report-templates', category: 'Zentral', translationKey: 'menu.reports' },
]; ];
if (activeAppCodes.includes('warehouse')) { if (activeAppCodes.includes('warehouse')) {
opts.push( opts.push(
{ label: t('menu.warehouse') + ' Dashboard', path: '/warehouse', category: t('menu.warehouse') }, { label: t('menu.warehouse') + ' ' + t('menu.dashboard'), path: '/warehouse', category: t('menu.warehouse'), translationKey: 'menu.warehouse' },
{ label: t('menu.articles'), path: '/warehouse/articles', category: t('menu.warehouse') }, { label: t('menu.articles'), path: '/warehouse/articles', category: t('menu.warehouse'), translationKey: 'menu.articles' },
{ label: t('menu.location'), path: '/warehouse/locations', category: t('menu.warehouse') }, { label: t('menu.location'), path: '/warehouse/locations', category: t('menu.warehouse'), translationKey: 'menu.location' },
{ label: 'Movimenti', path: '/warehouse/movements', category: t('menu.warehouse') }, { label: t('menu.movements'), path: '/warehouse/movements', category: t('menu.warehouse'), translationKey: 'menu.movements' },
{ label: 'Giacenze', path: '/warehouse/stock', category: t('menu.warehouse') }, { label: t('menu.stock'), path: '/warehouse/stock', category: t('menu.warehouse'), translationKey: 'menu.stock' },
{ label: 'Inventario', path: '/warehouse/inventory', category: t('menu.warehouse') } { label: t('menu.inventory'), path: '/warehouse/inventory', category: t('menu.warehouse'), translationKey: 'menu.inventory' }
); );
} }
if (activeAppCodes.includes('purchases')) { if (activeAppCodes.includes('purchases')) {
opts.push( opts.push(
{ label: 'Fornitori', path: '/purchases/suppliers', category: t('menu.purchases') }, { label: t('menu.suppliers'), path: '/purchases/suppliers', category: t('menu.purchases'), translationKey: 'menu.suppliers' },
{ label: 'Ordini Acquisto', path: '/purchases/orders', category: t('menu.purchases') } { label: t('menu.purchaseOrders'), path: '/purchases/orders', category: t('menu.purchases'), translationKey: 'menu.purchaseOrders' }
); );
} }
if (activeAppCodes.includes('sales')) { if (activeAppCodes.includes('sales')) {
opts.push( opts.push(
{ label: 'Ordini Vendita', path: '/sales/orders', category: t('menu.sales') } { label: t('menu.salesOrders'), path: '/sales/orders', category: t('menu.sales'), translationKey: 'menu.salesOrders' }
); );
} }
if (activeAppCodes.includes('production')) { if (activeAppCodes.includes('production')) {
opts.push( opts.push(
{ label: t('menu.production') + ' Dashboard', path: '/production', category: t('menu.production') }, { label: t('menu.production') + ' ' + t('menu.dashboard'), path: '/production', category: t('menu.production'), translationKey: 'menu.production' },
{ label: 'Ordini Produzione', path: '/production/orders', category: t('menu.production') }, { label: t('menu.productionOrders'), path: '/production/orders', category: t('menu.production'), translationKey: 'menu.productionOrders' },
{ label: 'Distinte Base', path: '/production/bom', category: t('menu.production') }, { label: t('menu.bom'), path: '/production/bom', category: t('menu.production'), translationKey: 'menu.bom' },
{ label: 'Centri di Lavoro', path: '/production/work-centers', category: t('menu.production') }, { label: t('menu.workCenters'), path: '/production/work-centers', category: t('menu.production'), translationKey: 'menu.workCenters' },
{ label: 'Cicli', path: '/production/cycles', category: t('menu.production') }, { label: t('menu.cycles'), path: '/production/cycles', category: t('menu.production'), translationKey: 'menu.cycles' },
{ label: 'MRP', path: '/production/mrp', category: t('menu.production') } { label: t('menu.mrp'), path: '/production/mrp', category: t('menu.production'), translationKey: 'menu.mrp' }
); );
} }
opts.push( opts.push(
{ label: t('menu.apps'), path: '/apps', category: 'Admin' }, { label: t('menu.apps'), path: '/apps', category: t('menu.administration'), translationKey: 'menu.apps' },
{ label: t('menu.autoCodes'), path: '/admin/auto-codes', category: 'Admin' }, { label: t('menu.autoCodes'), path: '/admin/auto-codes', category: t('menu.administration'), translationKey: 'menu.autoCodes' },
{ label: t('menu.customFields'), path: '/admin/custom-fields', category: 'Admin' } { label: t('menu.customFields'), path: '/admin/custom-fields', category: t('menu.administration'), translationKey: 'menu.customFields' }
); );
return opts; return opts;
@@ -128,7 +129,7 @@ export default function SearchBar() {
getOptionLabel={(option) => typeof option === 'string' ? option : option.label} getOptionLabel={(option) => typeof option === 'string' ? option : option.label}
onChange={(_, value) => { onChange={(_, value) => {
if (typeof value !== 'string' && value) { if (typeof value !== 'string' && value) {
openTab(value.path, value.label); openTab(value.path, value.label, true, value.translationKey);
} }
}} }}
renderInput={(params) => { renderInput={(params) => {
@@ -141,7 +142,7 @@ export default function SearchBar() {
<StyledInputBase <StyledInputBase
{...InputProps} {...InputProps}
{...rest} {...rest}
placeholder="Search..." placeholder={t('navigation.searchPlaceholder')}
inputProps={{ ...params.inputProps, 'aria-label': 'search' }} inputProps={{ ...params.inputProps, 'aria-label': 'search' }}
/> />
</Search> </Search>

View File

@@ -54,6 +54,7 @@ interface MenuItem {
path?: string; path?: string;
children?: MenuItem[]; children?: MenuItem[];
appCode?: string; appCode?: string;
translationKey?: string;
} }
interface SidebarProps { interface SidebarProps {
@@ -90,7 +91,7 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
} }
if (item.path) { if (item.path) {
openTab(item.path, item.tabLabel || item.label); openTab(item.path, item.tabLabel || item.label, true, item.translationKey);
if (onClose) onClose(); if (onClose) onClose();
} else if (item.children) { } else if (item.children) {
handleToggle(item.id); handleToggle(item.id);
@@ -103,19 +104,21 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: 'Zentral Dashboard', label: 'Zentral Dashboard',
icon: <DashboardIcon />, icon: <DashboardIcon />,
path: '/', path: '/',
translationKey: 'menu.dashboard',
}, },
{ {
id: 'warehouse', id: 'warehouse',
label: t('menu.warehouse'), label: t('menu.warehouse'),
icon: <WarehouseIcon />, icon: <WarehouseIcon />,
appCode: 'warehouse', appCode: 'warehouse',
translationKey: 'menu.warehouse',
children: [ children: [
{ id: 'wh-dashboard', label: 'Dashboard', tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse' }, { id: 'wh-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse', translationKey: 'menu.warehouse' },
{ id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles' }, { id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles', translationKey: 'menu.articles' },
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations' }, { id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations', translationKey: 'menu.location' },
{ id: 'wh-movements', label: 'Movimenti', icon: <SwapIcon />, path: '/warehouse/movements' }, { id: 'wh-movements', label: t('menu.movements'), icon: <SwapIcon />, path: '/warehouse/movements', translationKey: 'menu.movements' },
{ id: 'wh-stock', label: 'Giacenze', icon: <StorageIcon />, path: '/warehouse/stock' }, { id: 'wh-stock', label: t('menu.stock'), icon: <StorageIcon />, path: '/warehouse/stock', translationKey: 'menu.stock' },
{ id: 'wh-inventory', label: 'Inventario', icon: <AssignmentIcon />, path: '/warehouse/inventory' }, { id: 'wh-inventory', label: t('menu.inventory'), icon: <AssignmentIcon />, path: '/warehouse/inventory', translationKey: 'menu.inventory' },
], ],
}, },
{ {
@@ -123,9 +126,10 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: t('menu.purchases'), label: t('menu.purchases'),
icon: <ShoppingCartIcon />, icon: <ShoppingCartIcon />,
appCode: 'purchases', appCode: 'purchases',
translationKey: 'menu.purchases',
children: [ children: [
{ id: 'pur-suppliers', label: 'Fornitori', icon: <PeopleIcon />, path: '/purchases/suppliers' }, { id: 'pur-suppliers', label: t('menu.suppliers'), icon: <PeopleIcon />, path: '/purchases/suppliers', translationKey: 'menu.suppliers' },
{ id: 'pur-orders', label: 'Ordini Acquisto', icon: <ListAltIcon />, path: '/purchases/orders' }, { id: 'pur-orders', label: t('menu.purchaseOrders'), icon: <ListAltIcon />, path: '/purchases/orders', translationKey: 'menu.purchaseOrders' },
], ],
}, },
{ {
@@ -133,8 +137,9 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: t('menu.sales'), label: t('menu.sales'),
icon: <SellIcon />, icon: <SellIcon />,
appCode: 'sales', appCode: 'sales',
translationKey: 'menu.sales',
children: [ children: [
{ id: 'sal-orders', label: 'Ordini Vendita', icon: <ListAltIcon />, path: '/sales/orders' }, { id: 'sal-orders', label: t('menu.salesOrders'), icon: <ListAltIcon />, path: '/sales/orders', translationKey: 'menu.salesOrders' },
], ],
}, },
{ {
@@ -142,13 +147,14 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: t('menu.production'), label: t('menu.production'),
icon: <ProductionIcon />, icon: <ProductionIcon />,
appCode: 'production', appCode: 'production',
translationKey: 'menu.production',
children: [ children: [
{ id: 'prod-dashboard', label: 'Dashboard', tabLabel: t('menu.production'), icon: <DashboardIcon />, path: '/production' }, { id: 'prod-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.production'), icon: <DashboardIcon />, path: '/production', translationKey: 'menu.production' },
{ id: 'prod-orders', label: 'Ordini Produzione', icon: <ListAltIcon />, path: '/production/orders' }, { id: 'prod-orders', label: t('menu.productionOrders'), icon: <ListAltIcon />, path: '/production/orders', translationKey: 'menu.productionOrders' },
{ id: 'prod-bom', label: 'Distinte Base', icon: <AssignmentIcon />, path: '/production/bom' }, { id: 'prod-bom', label: t('menu.bom'), icon: <AssignmentIcon />, path: '/production/bom', translationKey: 'menu.bom' },
{ id: 'prod-workcenters', label: 'Centri di Lavoro', icon: <BuildIcon />, path: '/production/work-centers' }, { id: 'prod-workcenters', label: t('menu.workCenters'), icon: <BuildIcon />, path: '/production/work-centers', translationKey: 'menu.workCenters' },
{ id: 'prod-cycles', label: 'Cicli', icon: <TimelineIcon />, path: '/production/cycles' }, { id: 'prod-cycles', label: t('menu.cycles'), icon: <TimelineIcon />, path: '/production/cycles', translationKey: 'menu.cycles' },
{ id: 'prod-mrp', label: 'MRP', icon: <ManufacturingIcon />, path: '/production/mrp' }, { id: 'prod-mrp', label: t('menu.mrp'), icon: <ManufacturingIcon />, path: '/production/mrp', translationKey: 'menu.mrp' },
], ],
}, },
{ {
@@ -156,10 +162,11 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: t('menu.events'), label: t('menu.events'),
icon: <EventIcon />, icon: <EventIcon />,
appCode: 'events', appCode: 'events',
translationKey: 'menu.events',
children: [ children: [
{ id: 'ev-list', label: t('menu.events'), icon: <EventIcon />, path: '/events/list' }, { id: 'ev-list', label: t('menu.events'), icon: <EventIcon />, path: '/events/list', translationKey: 'menu.events' },
{ id: 'ev-calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/events/calendar' }, { id: 'ev-calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/events/calendar', translationKey: 'menu.calendar' },
{ id: 'ev-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/events/locations' }, { id: 'ev-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/events/locations', translationKey: 'menu.location' },
], ],
}, },
{ {
@@ -167,23 +174,25 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: t('apps.hr.title'), label: t('apps.hr.title'),
icon: <PeopleIcon />, icon: <PeopleIcon />,
appCode: 'hr', appCode: 'hr',
translationKey: 'apps.hr.title',
children: [ children: [
{ id: 'hr-dipendenti', label: t('apps.hr.dipendenti'), icon: <PeopleIcon />, path: '/hr/dipendenti' }, { id: 'hr-dipendenti', label: t('apps.hr.dipendenti'), icon: <PeopleIcon />, path: '/hr/dipendenti', translationKey: 'apps.hr.dipendenti' },
{ id: 'hr-contratti', label: t('apps.hr.contratti'), icon: <AssignmentIcon />, path: '/hr/contratti' }, { id: 'hr-contratti', label: t('apps.hr.contratti'), icon: <AssignmentIcon />, path: '/hr/contratti', translationKey: 'apps.hr.contratti' },
{ id: 'hr-assenze', label: t('apps.hr.assenze'), icon: <EventIcon />, path: '/hr/assenze' }, { id: 'hr-assenze', label: t('apps.hr.assenze'), icon: <EventIcon />, path: '/hr/assenze', translationKey: 'apps.hr.assenze' },
{ id: 'hr-pagamenti', label: t('apps.hr.pagamenti'), icon: <AttachMoneyIcon />, path: '/hr/pagamenti' }, { id: 'hr-pagamenti', label: t('apps.hr.pagamenti'), icon: <AttachMoneyIcon />, path: '/hr/pagamenti', translationKey: 'apps.hr.pagamenti' },
{ id: 'hr-rimborsi', label: t('apps.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi' }, { id: 'hr-rimborsi', label: t('apps.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi', translationKey: 'apps.hr.rimborsi' },
], ],
}, },
{ {
id: 'admin', id: 'admin',
label: 'Amministrazione', label: t('menu.administration'),
icon: <SettingsIcon />, icon: <SettingsIcon />,
translationKey: 'menu.administration',
children: [ children: [
{ id: 'apps', label: t('menu.apps'), icon: <ModulesIcon />, path: '/apps' }, { id: 'apps', label: t('menu.apps'), icon: <ModulesIcon />, path: '/apps', translationKey: 'menu.apps' },
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes' }, { id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes', translationKey: 'menu.autoCodes' },
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields' }, { id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields', translationKey: 'menu.customFields' },
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer' }, { id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer', translationKey: 'menu.reports' },
], ],
}, },
]; ];

View File

@@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { useLanguage } from '../contexts/LanguageContext';
interface SortableTabProps { interface SortableTabProps {
tab: TabType; tab: TabType;
@@ -16,6 +17,7 @@ interface SortableTabProps {
} }
function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }: SortableTabProps) { function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }: SortableTabProps) {
const { t } = useLanguage();
const { const {
attributes, attributes,
listeners, listeners,
@@ -52,7 +54,7 @@ function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }:
<Tab <Tab
label={ label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{tab.label}</span> <span>{tab.translationKey ? t(tab.translationKey) : tab.label}</span>
{tab.closable && ( {tab.closable && (
<IconButton <IconButton
size="small" size="small"
@@ -100,6 +102,7 @@ function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }:
} }
export default function TabsBar() { export default function TabsBar() {
const { t } = useLanguage();
const { const {
tabs, tabs,
activeTabPath, activeTabPath,
@@ -207,7 +210,7 @@ export default function TabsBar() {
{/* Tab Groups & Actions */} {/* Tab Groups & Actions */}
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, borderLeft: 1, borderColor: 'divider' }}> <Box sx={{ display: 'flex', alignItems: 'center', px: 1, borderLeft: 1, borderColor: 'divider' }}>
<Tooltip title="Tab Groups"> <Tooltip title={t('navigation.tabGroups')}>
<IconButton <IconButton
size="small" size="small"
onClick={(e) => setGroupsMenuAnchor(e.currentTarget)} onClick={(e) => setGroupsMenuAnchor(e.currentTarget)}
@@ -233,21 +236,21 @@ export default function TabsBar() {
handleCloseContextMenu(); handleCloseContextMenu();
}} disabled={!contextMenu?.tab?.closable}> }} disabled={!contextMenu?.tab?.closable}>
<ListItemIcon><CloseIcon fontSize="small" /></ListItemIcon> <ListItemIcon><CloseIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close</ListItemText> <ListItemText>{t('navigation.close')}</ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => { <MenuItem onClick={() => {
if (contextMenu?.tab) closeOtherTabs(contextMenu.tab.path); if (contextMenu?.tab) closeOtherTabs(contextMenu.tab.path);
handleCloseContextMenu(); handleCloseContextMenu();
}}> }}>
<ListItemIcon><ClearAllIcon fontSize="small" /></ListItemIcon> <ListItemIcon><ClearAllIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close Others</ListItemText> <ListItemText>{t('navigation.closeOthers')}</ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => { <MenuItem onClick={() => {
if (contextMenu?.tab) closeTabsToRight(contextMenu.tab.path); if (contextMenu?.tab) closeTabsToRight(contextMenu.tab.path);
handleCloseContextMenu(); handleCloseContextMenu();
}}> }}>
<ListItemIcon><ArrowForwardIcon fontSize="small" /></ListItemIcon> <ListItemIcon><ArrowForwardIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close to the Right</ListItemText> <ListItemText>{t('navigation.closeRight')}</ListItemText>
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -262,12 +265,12 @@ export default function TabsBar() {
setGroupsMenuAnchor(null); setGroupsMenuAnchor(null);
}}> }}>
<ListItemIcon><SaveIcon fontSize="small" /></ListItemIcon> <ListItemIcon><SaveIcon fontSize="small" /></ListItemIcon>
<ListItemText>Save Current Session</ListItemText> <ListItemText>{t('navigation.saveSession')}</ListItemText>
</MenuItem> </MenuItem>
<Divider /> <Divider />
{tabGroups.length === 0 && ( {tabGroups.length === 0 && (
<MenuItem disabled> <MenuItem disabled>
<ListItemText secondary="No saved groups" /> <ListItemText secondary={t('navigation.noSavedGroups')} />
</MenuItem> </MenuItem>
)} )}
{tabGroups.map((group) => ( {tabGroups.map((group) => (
@@ -292,12 +295,12 @@ export default function TabsBar() {
{/* Save Group Dialog */} {/* Save Group Dialog */}
<Dialog open={saveGroupDialogOpen} onClose={() => setSaveGroupDialogOpen(false)}> <Dialog open={saveGroupDialogOpen} onClose={() => setSaveGroupDialogOpen(false)}>
<DialogTitle>Save Tab Group</DialogTitle> <DialogTitle>{t('navigation.saveGroupTitle')}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
label="Group Name" label={t('navigation.groupName')}
fullWidth fullWidth
variant="outlined" variant="outlined"
value={newGroupName} value={newGroupName}
@@ -305,8 +308,8 @@ export default function TabsBar() {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setSaveGroupDialogOpen(false)}>Cancel</Button> <Button onClick={() => setSaveGroupDialogOpen(false)}>{t('navigation.cancel')}</Button>
<Button onClick={handleSaveGroup} variant="contained">Save</Button> <Button onClick={handleSaveGroup} variant="contained">{t('navigation.save')}</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Box> </Box>

View File

@@ -4,6 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
export interface Tab { export interface Tab {
path: string; path: string;
label: string; label: string;
translationKey?: string;
closable?: boolean; closable?: boolean;
} }
@@ -16,7 +17,7 @@ interface TabContextType {
tabs: Tab[]; tabs: Tab[];
activeTabPath: string; activeTabPath: string;
tabGroups: TabGroup[]; tabGroups: TabGroup[];
openTab: (path: string, label: string, closable?: boolean) => void; openTab: (path: string, label: string, closable?: boolean, translationKey?: string) => void;
closeTab: (path: string) => void; closeTab: (path: string) => void;
setActiveTab: (path: string) => void; setActiveTab: (path: string) => void;
reorderTabs: (newTabs: Tab[]) => void; reorderTabs: (newTabs: Tab[]) => void;
@@ -48,14 +49,14 @@ export function TabProvider({ children }: { children: ReactNode }) {
if (Array.isArray(parsedTabs) && parsedTabs.length > 0) { if (Array.isArray(parsedTabs) && parsedTabs.length > 0) {
setTabs(parsedTabs); setTabs(parsedTabs);
} else { } else {
setTabs([{ path: '/', label: 'Dashboard', closable: false }]); setTabs([{ path: '/', label: 'Dashboard', translationKey: 'menu.dashboard', closable: false }]);
} }
} catch (e) { } catch (e) {
console.error("Failed to parse tabs", e); console.error("Failed to parse tabs", e);
setTabs([{ path: '/', label: 'Dashboard', closable: false }]); setTabs([{ path: '/', label: 'Dashboard', translationKey: 'menu.dashboard', closable: false }]);
} }
} else { } else {
setTabs([{ path: '/', label: 'Dashboard', closable: false }]); setTabs([{ path: '/', label: 'Dashboard', translationKey: 'menu.dashboard', closable: false }]);
} }
if (savedActiveTab) { if (savedActiveTab) {
@@ -96,18 +97,19 @@ export function TabProvider({ children }: { children: ReactNode }) {
} }
}, [location.pathname, tabs]); }, [location.pathname, tabs]);
const openTab = (path: string, label: string, closable: boolean = true) => { const openTab = (path: string, label: string, closable: boolean = true, translationKey?: string) => {
setTabs((prev) => { setTabs((prev) => {
const existingTabIndex = prev.findIndex((t) => t.path === path); const existingTabIndex = prev.findIndex((t) => t.path === path);
if (existingTabIndex !== -1) { if (existingTabIndex !== -1) {
if (prev[existingTabIndex].label !== label) { // Update label and translationKey if they changed
if (prev[existingTabIndex].label !== label || prev[existingTabIndex].translationKey !== translationKey) {
const newTabs = [...prev]; const newTabs = [...prev];
newTabs[existingTabIndex] = { ...newTabs[existingTabIndex], label }; newTabs[existingTabIndex] = { ...newTabs[existingTabIndex], label, translationKey };
return newTabs; return newTabs;
} }
return prev; return prev;
} }
return [...prev, { path, label, closable }]; return [...prev, { path, label, closable, translationKey }];
}); });
navigate(path); navigate(path);
}; };