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.
- [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.
- [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.

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",
"apps": "Apps",
"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": {
"title": "Dashboard",
@@ -515,6 +540,135 @@
"saving": "Saving...",
"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": {
@@ -1072,17 +1226,19 @@
}
},
"purchases": {
"menu": {
"suppliers": "Suppliers",
"orders": "Purchase Orders"
"stats": {
"title": "Purchases",
"costsThisMonth": "Costs this month",
"pendingOrders": "{{count}} Pending Orders"
},
"suppliers": {
"supplier": {
"title": "Suppliers",
"newSupplier": "New Supplier",
"editSupplier": "Edit Supplier",
"createTitle": "New Supplier",
"editTitle": "Edit Supplier",
"columns": {
"code": "Code",
"name": "Name",
"name": "Business Name",
"vatNumber": "VAT Number",
"email": "Email",
"phone": "Phone",
@@ -1090,85 +1246,60 @@
"status": "Status"
},
"fields": {
"code": "Code",
"name": "Business Name",
"vatNumber": "VAT Number",
"fiscalCode": "Fiscal Code",
"email": "Email",
"pec": "PEC",
"phone": "Phone",
"website": "Website",
"address": "Address",
"city": "City",
"province": "Province",
"zipCode": "ZIP Code",
"country": "Country",
"email": "Email",
"pec": "PEC",
"phone": "Phone",
"website": "Website",
"paymentTerms": "Payment Terms",
"notes": "Notes",
"isActive": "Active"
"notes": "Notes"
}
},
"placeholders": {
"search": "Search supplier...",
"generatedAutomatically": "Generated automatically"
},
"deleteConfirm": "Are you sure you want to delete this supplier?"
},
"orders": {
"order": {
"title": "Purchase Orders",
"newOrder": "New Order",
"editOrder": "Edit Order",
"columns": {
"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"
},
"createTitle": "New Order",
"editTitle": "Edit Order",
"status": {
"Draft": "Draft",
"Confirmed": "Confirmed",
"PartiallyReceived": "Partially Received",
"Received": "Received",
"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": {
"addLine": "Add Line",
"confirm": "Confirm Order",
"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"
"receive": "Receive Goods"
}
}
},
@@ -1393,6 +1524,21 @@
"rimborsiTitle": "Reimbursement Management",
"newRimborso": "New 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",
"apps": "Applicazioni",
"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": {
"title": "Dashboard",
@@ -591,9 +616,143 @@
"saving": "Salvataggio...",
"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": {
"stats": {
"title": "Acquisti",
"costsThisMonth": "Costi questo mese",
"pendingOrders": "{{count}} Ordini in attesa"
},
"supplier": {
"title": "Fornitori",
"newSupplier": "Nuovo Fornitore",
@@ -1446,6 +1605,21 @@
"rimborsiTitle": "Gestione Rimborsi",
"newRimborso": "Nuovo 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 })}
required
>
<MenuItem value="Ferie">Ferie</MenuItem>
<MenuItem value="Malattia">Malattia</MenuItem>
<MenuItem value="Permesso">Permesso</MenuItem>
<MenuItem value="Altro">Altro</MenuItem>
<MenuItem value="Ferie">{t('personale.assenza.ferie')}</MenuItem>
<MenuItem value="Malattia">{t('personale.assenza.malattia')}</MenuItem>
<MenuItem value="Permesso">{t('personale.assenza.permesso')}</MenuItem>
<MenuItem value="Altro">{t('personale.assenza.altro')}</MenuItem>
</TextField>
</Grid>
<Grid size={6}>
@@ -236,9 +236,9 @@ export default function AssenzePage() {
onChange={(e) => setFormData({ ...formData, stato: e.target.value })}
required
>
<MenuItem value="Richiesta">Richiesta</MenuItem>
<MenuItem value="Approvata">Approvata</MenuItem>
<MenuItem value="Rifiutata">Rifiutata</MenuItem>
<MenuItem value="Richiesta">{t('personale.status.richiesta')}</MenuItem>
<MenuItem value="Approvata">{t('personale.status.approvata')}</MenuItem>
<MenuItem value="Rifiutata">{t('personale.status.rifiutata')}</MenuItem>
</TextField>
</Grid>
<Grid size={6}>

View File

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

View File

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

View File

@@ -184,11 +184,11 @@ export default function PurchaseOrderFormPage() {
</Button>
<Box>
<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>
{isEdit && order && (
<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"}
size="small"
sx={{ mt: 1 }}
@@ -206,7 +206,7 @@ export default function PurchaseOrderFormPage() {
onClick={() => confirmMutation.mutate(Number(id))}
disabled={confirmMutation.isPending}
>
{t("purchases.orders.actions.confirm")}
{t("purchases.order.actions.confirm")}
</Button>
)}
@@ -218,7 +218,7 @@ export default function PurchaseOrderFormPage() {
onClick={() => receiveMutation.mutate(Number(id))}
disabled={receiveMutation.isPending}
>
{t("purchases.orders.actions.receive")}
{t("purchases.order.actions.receive")}
</Button>
)}
@@ -249,7 +249,7 @@ export default function PurchaseOrderFormPage() {
control={control}
render={({ field }: { field: any }) => (
<DatePicker
label={t("purchases.orders.fields.orderDate")}
label={t("purchases.order.fields.date")}
value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly}
@@ -264,7 +264,7 @@ export default function PurchaseOrderFormPage() {
control={control}
render={({ field }: { field: any }) => (
<DatePicker
label={t("purchases.orders.fields.expectedDeliveryDate")}
label={t("purchases.order.fields.expectedDate")}
value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly}
@@ -288,7 +288,7 @@ export default function PurchaseOrderFormPage() {
renderInput={(params) => (
<TextField
{...params}
label={t("purchases.orders.fields.supplier")}
label={t("purchases.order.fields.supplier")}
error={!!errors.supplierId}
helperText={errors.supplierId?.message}
/>
@@ -311,7 +311,7 @@ export default function PurchaseOrderFormPage() {
renderInput={(params) => (
<TextField
{...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 }) => (
<TextField
{...field}
label={t("purchases.orders.fields.notes")}
label={t("purchases.order.fields.notes")}
fullWidth
disabled={isReadOnly}
/>
@@ -337,7 +337,7 @@ export default function PurchaseOrderFormPage() {
<Paper sx={{ p: 3 }}>
<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 && (
<Button startIcon={<AddIcon />} onClick={() => append({
warehouseArticleId: 0,
@@ -356,12 +356,12 @@ export default function PurchaseOrderFormPage() {
<Table>
<TableHead>
<TableRow>
<TableCell width="30%">{t("purchases.orders.fields.article")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.quantity")}</TableCell>
<TableCell width="15%">{t("purchases.orders.fields.unitPrice")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.discount")}</TableCell>
<TableCell width="10%">{t("purchases.orders.fields.taxRate")}</TableCell>
<TableCell width="15%" align="right">{t("purchases.orders.fields.lineTotal")}</TableCell>
<TableCell width="30%">{t("purchases.order.lines.article")}</TableCell>
<TableCell width="10%">{t("purchases.order.lines.quantity")}</TableCell>
<TableCell width="15%">{t("purchases.order.lines.price")}</TableCell>
<TableCell width="10%">{t("purchases.order.lines.discount")}</TableCell>
<TableCell width="10%">{t("purchases.order.lines.tax")}</TableCell>
<TableCell width="15%" align="right">{t("purchases.order.lines.total")}</TableCell>
<TableCell width="10%"></TableCell>
</TableRow>
</TableHead>
@@ -464,7 +464,7 @@ export default function PurchaseOrderFormPage() {
))}
<TableRow>
<TableCell colSpan={5} align="right">
<Typography fontWeight="bold">{t("purchases.orders.totals.gross")}</Typography>
<Typography fontWeight="bold">{t("purchases.order.total")}</Typography>
</TableCell>
<TableCell align="right">
<Typography fontWeight="bold">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,7 @@ interface MenuItem {
path?: string;
children?: MenuItem[];
appCode?: string;
translationKey?: string;
}
interface SidebarProps {
@@ -90,7 +91,7 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
}
if (item.path) {
openTab(item.path, item.tabLabel || item.label);
openTab(item.path, item.tabLabel || item.label, true, item.translationKey);
if (onClose) onClose();
} else if (item.children) {
handleToggle(item.id);
@@ -103,19 +104,21 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
label: 'Zentral Dashboard',
icon: <DashboardIcon />,
path: '/',
translationKey: 'menu.dashboard',
},
{
id: 'warehouse',
label: t('menu.warehouse'),
icon: <WarehouseIcon />,
appCode: 'warehouse',
translationKey: 'menu.warehouse',
children: [
{ id: 'wh-dashboard', label: 'Dashboard', tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse' },
{ id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles' },
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations' },
{ id: 'wh-movements', label: 'Movimenti', icon: <SwapIcon />, path: '/warehouse/movements' },
{ id: 'wh-stock', label: 'Giacenze', icon: <StorageIcon />, path: '/warehouse/stock' },
{ id: 'wh-inventory', label: 'Inventario', icon: <AssignmentIcon />, path: '/warehouse/inventory' },
{ 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', translationKey: 'menu.articles' },
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations', translationKey: 'menu.location' },
{ id: 'wh-movements', label: t('menu.movements'), icon: <SwapIcon />, path: '/warehouse/movements', translationKey: 'menu.movements' },
{ id: 'wh-stock', label: t('menu.stock'), icon: <StorageIcon />, path: '/warehouse/stock', translationKey: 'menu.stock' },
{ 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'),
icon: <ShoppingCartIcon />,
appCode: 'purchases',
translationKey: 'menu.purchases',
children: [
{ id: 'pur-suppliers', label: 'Fornitori', icon: <PeopleIcon />, path: '/purchases/suppliers' },
{ id: 'pur-orders', label: 'Ordini Acquisto', icon: <ListAltIcon />, path: '/purchases/orders' },
{ id: 'pur-suppliers', label: t('menu.suppliers'), icon: <PeopleIcon />, path: '/purchases/suppliers', translationKey: 'menu.suppliers' },
{ 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'),
icon: <SellIcon />,
appCode: 'sales',
translationKey: 'menu.sales',
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'),
icon: <ProductionIcon />,
appCode: 'production',
translationKey: 'menu.production',
children: [
{ id: 'prod-dashboard', label: 'Dashboard', tabLabel: t('menu.production'), icon: <DashboardIcon />, path: '/production' },
{ id: 'prod-orders', label: 'Ordini Produzione', icon: <ListAltIcon />, path: '/production/orders' },
{ id: 'prod-bom', label: 'Distinte Base', icon: <AssignmentIcon />, path: '/production/bom' },
{ id: 'prod-workcenters', label: 'Centri di Lavoro', icon: <BuildIcon />, path: '/production/work-centers' },
{ id: 'prod-cycles', label: 'Cicli', icon: <TimelineIcon />, path: '/production/cycles' },
{ id: 'prod-mrp', label: 'MRP', icon: <ManufacturingIcon />, path: '/production/mrp' },
{ id: 'prod-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.production'), icon: <DashboardIcon />, path: '/production', translationKey: 'menu.production' },
{ id: 'prod-orders', label: t('menu.productionOrders'), icon: <ListAltIcon />, path: '/production/orders', translationKey: 'menu.productionOrders' },
{ id: 'prod-bom', label: t('menu.bom'), icon: <AssignmentIcon />, path: '/production/bom', translationKey: 'menu.bom' },
{ id: 'prod-workcenters', label: t('menu.workCenters'), icon: <BuildIcon />, path: '/production/work-centers', translationKey: 'menu.workCenters' },
{ id: 'prod-cycles', label: t('menu.cycles'), icon: <TimelineIcon />, path: '/production/cycles', translationKey: 'menu.cycles' },
{ 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'),
icon: <EventIcon />,
appCode: 'events',
translationKey: 'menu.events',
children: [
{ id: 'ev-list', label: t('menu.events'), icon: <EventIcon />, path: '/events/list' },
{ id: 'ev-calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/events/calendar' },
{ id: 'ev-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/events/locations' },
{ 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', translationKey: 'menu.calendar' },
{ 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'),
icon: <PeopleIcon />,
appCode: 'hr',
translationKey: 'apps.hr.title',
children: [
{ id: 'hr-dipendenti', label: t('apps.hr.dipendenti'), icon: <PeopleIcon />, path: '/hr/dipendenti' },
{ id: 'hr-contratti', label: t('apps.hr.contratti'), icon: <AssignmentIcon />, path: '/hr/contratti' },
{ id: 'hr-assenze', label: t('apps.hr.assenze'), icon: <EventIcon />, path: '/hr/assenze' },
{ id: 'hr-pagamenti', label: t('apps.hr.pagamenti'), icon: <AttachMoneyIcon />, path: '/hr/pagamenti' },
{ id: 'hr-rimborsi', label: t('apps.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi' },
{ 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', translationKey: 'apps.hr.contratti' },
{ 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', translationKey: 'apps.hr.pagamenti' },
{ id: 'hr-rimborsi', label: t('apps.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi', translationKey: 'apps.hr.rimborsi' },
],
},
{
id: 'admin',
label: 'Amministrazione',
label: t('menu.administration'),
icon: <SettingsIcon />,
translationKey: 'menu.administration',
children: [
{ id: 'apps', label: t('menu.apps'), icon: <ModulesIcon />, path: '/apps' },
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes' },
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields' },
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer' },
{ 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', translationKey: 'menu.autoCodes' },
{ 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', 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 { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useLanguage } from '../contexts/LanguageContext';
interface SortableTabProps {
tab: TabType;
@@ -16,6 +17,7 @@ interface SortableTabProps {
}
function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }: SortableTabProps) {
const { t } = useLanguage();
const {
attributes,
listeners,
@@ -52,7 +54,7 @@ function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }:
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{tab.label}</span>
<span>{tab.translationKey ? t(tab.translationKey) : tab.label}</span>
{tab.closable && (
<IconButton
size="small"
@@ -100,6 +102,7 @@ function SortableTab({ tab, activeTabPath, onActivate, onClose, onContextMenu }:
}
export default function TabsBar() {
const { t } = useLanguage();
const {
tabs,
activeTabPath,
@@ -207,7 +210,7 @@ export default function TabsBar() {
{/* Tab Groups & Actions */}
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, borderLeft: 1, borderColor: 'divider' }}>
<Tooltip title="Tab Groups">
<Tooltip title={t('navigation.tabGroups')}>
<IconButton
size="small"
onClick={(e) => setGroupsMenuAnchor(e.currentTarget)}
@@ -233,21 +236,21 @@ export default function TabsBar() {
handleCloseContextMenu();
}} disabled={!contextMenu?.tab?.closable}>
<ListItemIcon><CloseIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close</ListItemText>
<ListItemText>{t('navigation.close')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
if (contextMenu?.tab) closeOtherTabs(contextMenu.tab.path);
handleCloseContextMenu();
}}>
<ListItemIcon><ClearAllIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close Others</ListItemText>
<ListItemText>{t('navigation.closeOthers')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
if (contextMenu?.tab) closeTabsToRight(contextMenu.tab.path);
handleCloseContextMenu();
}}>
<ListItemIcon><ArrowForwardIcon fontSize="small" /></ListItemIcon>
<ListItemText>Close to the Right</ListItemText>
<ListItemText>{t('navigation.closeRight')}</ListItemText>
</MenuItem>
</Menu>
@@ -262,12 +265,12 @@ export default function TabsBar() {
setGroupsMenuAnchor(null);
}}>
<ListItemIcon><SaveIcon fontSize="small" /></ListItemIcon>
<ListItemText>Save Current Session</ListItemText>
<ListItemText>{t('navigation.saveSession')}</ListItemText>
</MenuItem>
<Divider />
{tabGroups.length === 0 && (
<MenuItem disabled>
<ListItemText secondary="No saved groups" />
<ListItemText secondary={t('navigation.noSavedGroups')} />
</MenuItem>
)}
{tabGroups.map((group) => (
@@ -292,12 +295,12 @@ export default function TabsBar() {
{/* Save Group Dialog */}
<Dialog open={saveGroupDialogOpen} onClose={() => setSaveGroupDialogOpen(false)}>
<DialogTitle>Save Tab Group</DialogTitle>
<DialogTitle>{t('navigation.saveGroupTitle')}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Group Name"
label={t('navigation.groupName')}
fullWidth
variant="outlined"
value={newGroupName}
@@ -305,8 +308,8 @@ export default function TabsBar() {
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setSaveGroupDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSaveGroup} variant="contained">Save</Button>
<Button onClick={() => setSaveGroupDialogOpen(false)}>{t('navigation.cancel')}</Button>
<Button onClick={handleSaveGroup} variant="contained">{t('navigation.save')}</Button>
</DialogActions>
</Dialog>
</Box>

View File

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