feat: implement global translation for HR, purchases, and core UI components
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
},
|
||||
"placeholders": {
|
||||
"search": "Search supplier...",
|
||||
"generatedAutomatically": "Generated automatically"
|
||||
},
|
||||
"deleteConfirm": "Are you sure you want to delete this supplier?"
|
||||
"notes": "Notes"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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,27 +802,23 @@ 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>
|
||||
</Popover >
|
||||
|
||||
{/* Zoom Popover */}
|
||||
<Popover
|
||||
< 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 }}>
|
||||
@@ -850,8 +850,8 @@ export default function EditorToolbar({
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Paper>
|
||||
</Popover >
|
||||
</Paper >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user