-
This commit is contained in:
@@ -42,6 +42,14 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
||||
|
||||
**NON dimenticare:** Questo file è la memoria persistente tra sessioni. Se non viene aggiornato, il lavoro fatto andrà perso e dovrà essere riscoperto.
|
||||
|
||||
### Rispetto delle Traduzioni (i18n)
|
||||
|
||||
**OBBLIGATORIO:** È severamente vietato inserire stringhe hardcoded nel codice frontend.
|
||||
- **TUTTI** i testi visibili all'utente devono usare `useTranslation` e le chiavi definite in `translation.json`.
|
||||
- **Nuove stringhe:** Se serve un nuovo testo, aggiungerlo PRIMA in `it/translation.json` e `en/translation.json`, poi usarlo nel codice.
|
||||
- **Placeholder:** Usare i placeholder (es. `{{value}}`) per valori dinamici, mai concatenazione di stringhe.
|
||||
- **Date/Numeri:** Usare i formattatori di `react-i18next` o `Intl` per date e numeri.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start - Session Recovery
|
||||
@@ -65,6 +73,24 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
||||
- Persistenza impostazioni nel browser (localStorage)
|
||||
- Adattamento automatico componenti MUI
|
||||
|
||||
- **NUOVA FEATURE: Sistema Internazionalizzazione (i18n)** - COMPLETATO
|
||||
- **Obiettivo:** Implementare un sistema robusto per la gestione delle traduzioni (Italiano/Inglese)
|
||||
- **Stack Tecnologico:** `i18next`, `react-i18next`, `i18next-http-backend`, `i18next-browser-languagedetector`
|
||||
- **Implementazione:**
|
||||
- `i18n.ts` - Configurazione inizializzazione i18next con backend loader e detector
|
||||
- `public/locales/{lang}/translation.json` - File di traduzione JSON separati per lingua
|
||||
- Refactoring `LanguageContext.tsx` per usare `useTranslation` hook
|
||||
- Aggiornamento `Layout.tsx` e `SettingsSelector.tsx` per usare chiavi di traduzione
|
||||
- Traduzione completa delle pagine:
|
||||
- `Dashboard.tsx`
|
||||
- `EventiPage.tsx`
|
||||
- `ClientiPage.tsx`
|
||||
- **Vantaggi:**
|
||||
- Caricamento asincrono delle traduzioni
|
||||
- Rilevamento automatico lingua browser
|
||||
- Struttura scalabile per future lingue
|
||||
- Namespace per organizzazione chiavi (common, menu, modules)
|
||||
|
||||
|
||||
- **NUOVA FEATURE: Gestione Inventario (Frontend)** - COMPLETATO
|
||||
- **Obiettivo:** Interfaccia utente per la gestione completa degli inventari fisici
|
||||
@@ -90,6 +116,17 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
||||
- **Soluzione:** Corretti i percorsi in `useWarehouseNavigation.ts` per corrispondere a `routes.tsx`
|
||||
- **File modificati:** `frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts`
|
||||
|
||||
- **FIX: Traduzioni Warehouse non funzionanti** - RISOLTO
|
||||
- **Problema:** Le traduzioni del modulo warehouse mostravano i placeholder (chiavi) invece del testo
|
||||
- **Causa:** La sezione `warehouse` in `translation.json` era erroneamente annidata dentro l'oggetto `reports` invece di essere alla radice
|
||||
- **Soluzione:** Spostata la sezione `warehouse` al livello principale in `translation.json` (IT e EN)
|
||||
- **File modificati:** `frontend/public/locales/it/translation.json`, `frontend/public/locales/en/translation.json`
|
||||
|
||||
- **FIX: Traduzioni Custom Fields** - RISOLTO
|
||||
- **Problema:** Mancavano traduzioni per la pagina dei custom fields e per i tipi di campo
|
||||
- **Soluzione:** Aggiunte chiavi mancanti (`noFields`, `multiselect`, `sectionTitle`), corretto casing per entità warehouse, aggiornato codice per usare chiavi corrette
|
||||
- **File modificati:** `frontend/public/locales/it/translation.json`, `frontend/public/locales/en/translation.json`, `frontend/src/pages/CustomFieldsAdminPage.tsx`, `frontend/src/components/customFields/CustomFieldsRenderer.tsx`
|
||||
|
||||
- **FIX: Campo Codice Readonly e Codice Alternativo** - COMPLETATO
|
||||
- **Obiettivo:** Il campo "Codice" deve essere sempre auto-generato (non modificabile), aggiungere campo "Codice Alternativo" opzionale
|
||||
- **Backend modificato:**
|
||||
|
||||
109
frontend/package-lock.json
generated
109
frontend/package-lock.json
generated
@@ -26,8 +26,12 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"fabric": "^6.9.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"uuid": "^13.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
@@ -2878,6 +2882,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3957,6 +3970,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||
@@ -3986,6 +4008,55 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.6.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.3.tgz",
|
||||
"integrity": "sha512-AEQvoPDljhp67a1+NsnG/Wb1Nh6YoSvtrmeEd24sfGn3uujCtXCF3cXpr7ulhMywKNFF7p3TX1u2j7y+caLOJg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
||||
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
|
||||
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -4902,6 +4973,33 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.3.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz",
|
||||
"integrity": "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 25.6.2",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
|
||||
@@ -5421,7 +5519,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -5626,6 +5724,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
|
||||
|
||||
@@ -28,8 +28,12 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"fabric": "^6.9.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"uuid": "^13.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
|
||||
1049
frontend/public/locales/en/translation.json
Normal file
1049
frontend/public/locales/en/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
1128
frontend/public/locales/it/translation.json
Normal file
1128
frontend/public/locales/it/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,36 +33,19 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import CollaborationIndicator from "./collaboration/CollaborationIndicator";
|
||||
import { useModules } from "../contexts/ModuleContext";
|
||||
import { useLanguage } from "../contexts/LanguageContext";
|
||||
import { SettingsSelector } from "./SettingsSelector";
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
const DRAWER_WIDTH_COLLAPSED = 64;
|
||||
|
||||
const menuItems = [
|
||||
{ text: "Dashboard", icon: <DashboardIcon />, path: "/" },
|
||||
{ text: "Calendario", icon: <CalendarIcon />, path: "/calendario" },
|
||||
{ text: "Eventi", icon: <EventIcon />, path: "/eventi" },
|
||||
{ text: "Clienti", icon: <PeopleIcon />, path: "/clienti" },
|
||||
{ text: "Location", icon: <PlaceIcon />, path: "/location" },
|
||||
{ text: "Articoli", icon: <InventoryIcon />, path: "/articoli" },
|
||||
{ text: "Risorse", icon: <PersonIcon />, path: "/risorse" },
|
||||
{
|
||||
text: "Magazzino",
|
||||
icon: <WarehouseIcon />,
|
||||
path: "/warehouse",
|
||||
moduleCode: "warehouse",
|
||||
},
|
||||
{ text: "Report", icon: <PrintIcon />, path: "/report-templates" },
|
||||
{ text: "Moduli", icon: <ModulesIcon />, path: "/modules" },
|
||||
{ text: "Codici Auto", icon: <AutoCodeIcon />, path: "/admin/auto-codes" },
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const { activeModules } = useModules();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px
|
||||
@@ -71,6 +54,26 @@ export default function Layout() {
|
||||
// Drawer width based on screen size
|
||||
const drawerWidth = isTablet ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH;
|
||||
|
||||
const menuItems = [
|
||||
{ text: t('menu.dashboard'), icon: <DashboardIcon />, path: "/" },
|
||||
{ text: t('menu.calendar'), icon: <CalendarIcon />, path: "/calendario" },
|
||||
{ text: t('menu.events'), icon: <EventIcon />, path: "/eventi" },
|
||||
{ text: t('menu.clients'), icon: <PeopleIcon />, path: "/clienti" },
|
||||
{ text: t('menu.location'), icon: <PlaceIcon />, path: "/location" },
|
||||
{ text: t('menu.articles'), icon: <InventoryIcon />, path: "/articoli" },
|
||||
{ text: t('menu.resources'), icon: <PersonIcon />, path: "/risorse" },
|
||||
{
|
||||
text: t('menu.warehouse'),
|
||||
icon: <WarehouseIcon />,
|
||||
path: "/warehouse",
|
||||
moduleCode: "warehouse",
|
||||
},
|
||||
{ text: t('menu.reports'), icon: <PrintIcon />, path: "/report-templates" },
|
||||
{ text: t('menu.modules'), icon: <ModulesIcon />, path: "/modules" },
|
||||
{ text: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: "/admin/auto-codes" },
|
||||
{ text: t('menu.customFields'), icon: <AutoCodeIcon />, path: "/admin/custom-fields" },
|
||||
];
|
||||
|
||||
// Filter menu items based on active modules
|
||||
const activeModuleCodes = activeModules.map((m) => m.code);
|
||||
const filteredMenuItems = menuItems.filter(
|
||||
|
||||
@@ -46,7 +46,7 @@ export const SettingsSelector: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={t('settings')}>
|
||||
<Tooltip title={t('common.settings')}>
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
@@ -98,7 +98,7 @@ export const SettingsSelector: React.FC = () => {
|
||||
{mode === 'dark' ? <LightModeIcon fontSize="small" /> : <DarkModeIcon fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{mode === 'dark' ? t('light') : t('dark')}
|
||||
{mode === 'dark' ? t('common.light') : t('common.dark')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { CustomFieldDefinition, CustomFieldType, CustomFieldValues } from '../../types/customFields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { customFieldService } from '../../services/customFieldService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -20,6 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const CustomFieldsRenderer: React.FC<Props> = ({ entityName, values, onChange, readOnly = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const [definitions, setDefinitions] = useState<CustomFieldDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -43,7 +45,7 @@ export const CustomFieldsRenderer: React.FC<Props> = ({ entityName, values, onCh
|
||||
return (
|
||||
<Box sx={{ mt: 2, mb: 2, p: 2, border: '1px dashed #ccc', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle1" gutterBottom color="primary">
|
||||
Campi Personalizzati
|
||||
{t("customFields.sectionTitle")}
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
{definitions.map(def => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/it';
|
||||
import 'dayjs/locale/en';
|
||||
@@ -21,63 +22,19 @@ const LanguageContext = createContext<LanguageContextType>({
|
||||
|
||||
export const useLanguage = () => useContext(LanguageContext);
|
||||
|
||||
// Simple translation dictionary for demo purposes
|
||||
const translations: Record<Language, Record<string, string>> = {
|
||||
it: {
|
||||
'settings': 'Impostazioni',
|
||||
'theme': 'Tema',
|
||||
'language': 'Lingua',
|
||||
'dark': 'Scuro',
|
||||
'light': 'Chiaro',
|
||||
'logout': 'Esci',
|
||||
'dashboard': 'Dashboard',
|
||||
'calendar': 'Calendario',
|
||||
'events': 'Eventi',
|
||||
'clients': 'Clienti',
|
||||
'location': 'Location',
|
||||
'articles': 'Articoli',
|
||||
'resources': 'Risorse',
|
||||
'warehouse': 'Magazzino',
|
||||
'reports': 'Report',
|
||||
'modules': 'Moduli',
|
||||
'autoCodes': 'Codici Auto',
|
||||
},
|
||||
en: {
|
||||
'settings': 'Settings',
|
||||
'theme': 'Theme',
|
||||
'language': 'Language',
|
||||
'dark': 'Dark',
|
||||
'light': 'Light',
|
||||
'logout': 'Logout',
|
||||
'dashboard': 'Dashboard',
|
||||
'calendar': 'Calendar',
|
||||
'events': 'Events',
|
||||
'clients': 'Clients',
|
||||
'location': 'Location',
|
||||
'articles': 'Articles',
|
||||
'resources': 'Resources',
|
||||
'warehouse': 'Warehouse',
|
||||
'reports': 'Reports',
|
||||
'modules': 'Modules',
|
||||
'autoCodes': 'Auto Codes',
|
||||
}
|
||||
};
|
||||
|
||||
export const AppLanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [language, setLanguage] = useState<Language>(() => {
|
||||
const savedLang = localStorage.getItem('language');
|
||||
return (savedLang as Language) || 'it';
|
||||
});
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const language = (i18n.language?.split('-')[0] as Language) || 'it';
|
||||
|
||||
const setLanguage = (lang: Language) => {
|
||||
i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('language', language);
|
||||
dayjs.locale(language);
|
||||
}, [language]);
|
||||
|
||||
const t = (key: string) => {
|
||||
return translations[language][key] || key;
|
||||
};
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={language}>
|
||||
|
||||
35
frontend/src/i18n.ts
Normal file
35
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
|
||||
// learn more: https://github.com/i18next/i18next-http-backend
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languagedetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'it',
|
||||
debug: import.meta.env.DEV,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -1,10 +1,13 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { StrictMode, Suspense } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import './i18n'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Delete as DeleteIcon,
|
||||
Image as ImageIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useArticle,
|
||||
useCreateArticle,
|
||||
@@ -78,6 +79,7 @@ function TabPanel(props: TabPanelProps) {
|
||||
}
|
||||
|
||||
export default function ArticleFormPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isNew = !id || id === "new";
|
||||
@@ -194,13 +196,13 @@ export default function ArticleFormPage() {
|
||||
const newErrors: Record<string, string> = {};
|
||||
// Il codice è generato automaticamente, non richiede validazione in creazione
|
||||
if (!isNew && !formData.code.trim()) {
|
||||
newErrors.code = "Il codice è obbligatorio";
|
||||
newErrors.code = t("warehouse.articleForm.validation.codeRequired");
|
||||
}
|
||||
if (!formData.description.trim()) {
|
||||
newErrors.description = "La descrizione è obbligatoria";
|
||||
newErrors.description = t("warehouse.articleForm.validation.descriptionRequired");
|
||||
}
|
||||
if (!formData.unitOfMeasure.trim()) {
|
||||
newErrors.unitOfMeasure = "L'unità di misura è obbligatoria";
|
||||
newErrors.unitOfMeasure = t("warehouse.articleForm.validation.uomRequired");
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -300,24 +302,23 @@ export default function ArticleFormPage() {
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
{isNew ? "Nuovo Articolo" : `Articolo: ${article?.code}`}
|
||||
{isNew ? t("warehouse.articleForm.titleNew") : t("warehouse.articleForm.titleEdit", { code: article?.code })}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{(createMutation.error || updateMutation.error) && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
Errore durante il salvataggio:{" "}
|
||||
{((createMutation.error || updateMutation.error) as Error).message}
|
||||
{t("warehouse.articleForm.errors.saveError", { error: ((createMutation.error || updateMutation.error) as Error).message })}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!isNew && (
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}>
|
||||
<Tab label="Dati Generali" />
|
||||
<Tab label="Giacenze" />
|
||||
{article?.isBatchManaged && <Tab label="Lotti" />}
|
||||
{article?.isSerialManaged && <Tab label="Matricole" />}
|
||||
<Tab label={t("warehouse.articleForm.tabs.general")} />
|
||||
<Tab label={t("warehouse.articleForm.tabs.stock")} />
|
||||
{article?.isBatchManaged && <Tab label={t("warehouse.articleForm.tabs.batches")} />}
|
||||
{article?.isSerialManaged && <Tab label={t("warehouse.articleForm.tabs.serials")} />}
|
||||
</Tabs>
|
||||
</Box>
|
||||
)}
|
||||
@@ -329,21 +330,21 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Informazioni Base
|
||||
{t("warehouse.articleForm.sections.basicInfo")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice"
|
||||
label={t("warehouse.articleForm.fields.code")}
|
||||
value={
|
||||
isNew ? "(Generato al salvataggio)" : formData.code
|
||||
isNew ? t("warehouse.articleForm.helpers.generatedOnSave") : formData.code
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
isNew
|
||||
? "Verrà assegnato automaticamente"
|
||||
: "Generato automaticamente"
|
||||
? t("warehouse.articleForm.helpers.willBeGenerated")
|
||||
: t("warehouse.articleForm.helpers.generatedAutomatically")
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -351,11 +352,11 @@ export default function ArticleFormPage() {
|
||||
sx={
|
||||
isNew
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -363,18 +364,18 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice Alternativo"
|
||||
label={t("warehouse.articleForm.fields.alternativeCode")}
|
||||
value={formData.alternativeCode}
|
||||
onChange={(e) =>
|
||||
handleChange("alternativeCode", e.target.value)
|
||||
}
|
||||
helperText="Opzionale"
|
||||
helperText={t("warehouse.articleForm.helpers.optional")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Descrizione"
|
||||
label={t("warehouse.articleForm.fields.description")}
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
handleChange("description", e.target.value)
|
||||
@@ -387,7 +388,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Descrizione Breve"
|
||||
label={t("warehouse.articleForm.fields.shortDescription")}
|
||||
value={formData.shortDescription}
|
||||
onChange={(e) =>
|
||||
handleChange("shortDescription", e.target.value)
|
||||
@@ -396,10 +397,10 @@ export default function ArticleFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<InputLabel>{t("warehouse.articleForm.fields.category")}</InputLabel>
|
||||
<Select
|
||||
value={formData.categoryId || ""}
|
||||
label="Categoria"
|
||||
label={t("warehouse.articleForm.fields.category")}
|
||||
onChange={(e) =>
|
||||
handleChange(
|
||||
"categoryId",
|
||||
@@ -408,7 +409,7 @@ export default function ArticleFormPage() {
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Nessuna</em>
|
||||
<em>{t("warehouse.articleForm.options.noCategory")}</em>
|
||||
</MenuItem>
|
||||
{flatCategories.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
@@ -421,7 +422,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Unità di Misura"
|
||||
label={t("warehouse.articleForm.fields.uom")}
|
||||
value={formData.unitOfMeasure}
|
||||
onChange={(e) =>
|
||||
handleChange("unitOfMeasure", e.target.value)
|
||||
@@ -434,7 +435,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice a Barre"
|
||||
label={t("warehouse.articleForm.fields.barcode")}
|
||||
value={formData.barcode}
|
||||
onChange={(e) => handleChange("barcode", e.target.value)}
|
||||
/>
|
||||
@@ -444,13 +445,13 @@ export default function ArticleFormPage() {
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Livelli di Scorta
|
||||
{t("warehouse.articleForm.sections.stockLevels")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Scorta Minima"
|
||||
label={t("warehouse.articleForm.fields.minStock")}
|
||||
type="number"
|
||||
value={formData.minimumStock}
|
||||
onChange={(e) =>
|
||||
@@ -465,7 +466,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Scorta Massima"
|
||||
label={t("warehouse.articleForm.fields.maxStock")}
|
||||
type="number"
|
||||
value={formData.maximumStock}
|
||||
onChange={(e) =>
|
||||
@@ -480,7 +481,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Punto di Riordino"
|
||||
label={t("warehouse.articleForm.fields.reorderPoint")}
|
||||
type="number"
|
||||
value={formData.reorderPoint}
|
||||
onChange={(e) =>
|
||||
@@ -495,7 +496,7 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Quantità Riordino"
|
||||
label={t("warehouse.articleForm.fields.reorderQuantity")}
|
||||
type="number"
|
||||
value={formData.reorderQuantity}
|
||||
onChange={(e) =>
|
||||
@@ -512,13 +513,13 @@ export default function ArticleFormPage() {
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Costi e Valorizzazione
|
||||
{t("warehouse.articleForm.sections.costs")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Costo Standard"
|
||||
label={t("warehouse.articleForm.fields.standardCost")}
|
||||
type="number"
|
||||
value={formData.standardCost}
|
||||
onChange={(e) =>
|
||||
@@ -537,18 +538,18 @@ export default function ArticleFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Gestione Stock</InputLabel>
|
||||
<InputLabel>{t("warehouse.articleForm.fields.stockManagement")}</InputLabel>
|
||||
<Select
|
||||
value={formData.stockManagement}
|
||||
label="Gestione Stock"
|
||||
label={t("warehouse.articleForm.fields.stockManagement")}
|
||||
onChange={(e) =>
|
||||
handleChange("stockManagement", e.target.value)
|
||||
}
|
||||
>
|
||||
{Object.entries(stockManagementTypeLabels).map(
|
||||
([value, label]) => (
|
||||
([value]) => (
|
||||
<MenuItem key={value} value={parseInt(value, 10)}>
|
||||
{label}
|
||||
{t(`warehouse.stockManagementType.${StockManagementType[parseInt(value, 10)]}`)}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
@@ -557,18 +558,18 @@ export default function ArticleFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Metodo di Valorizzazione</InputLabel>
|
||||
<InputLabel>{t("warehouse.articleForm.fields.valuationMethod")}</InputLabel>
|
||||
<Select
|
||||
value={formData.valuationMethod}
|
||||
label="Metodo di Valorizzazione"
|
||||
label={t("warehouse.articleForm.fields.valuationMethod")}
|
||||
onChange={(e) =>
|
||||
handleChange("valuationMethod", e.target.value)
|
||||
}
|
||||
>
|
||||
{Object.entries(valuationMethodLabels).map(
|
||||
([value, label]) => (
|
||||
([value]) => (
|
||||
<MenuItem key={value} value={parseInt(value, 10)}>
|
||||
{label}
|
||||
{t(`warehouse.valuationMethod.${ValuationMethod[parseInt(value, 10)]}`)}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
@@ -580,7 +581,7 @@ export default function ArticleFormPage() {
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Tracciabilità
|
||||
{t("warehouse.articleForm.sections.traceability")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
@@ -594,7 +595,7 @@ export default function ArticleFormPage() {
|
||||
disabled={!isNew && article?.isBatchManaged}
|
||||
/>
|
||||
}
|
||||
label="Gestione Lotti"
|
||||
label={t("warehouse.articleForm.fields.batchManaged")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
@@ -608,7 +609,7 @@ export default function ArticleFormPage() {
|
||||
disabled={!isNew && article?.isSerialManaged}
|
||||
/>
|
||||
}
|
||||
label="Gestione Matricole"
|
||||
label={t("warehouse.articleForm.fields.serialManaged")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
@@ -621,7 +622,7 @@ export default function ArticleFormPage() {
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Gestione Scadenza"
|
||||
label={t("warehouse.articleForm.fields.expiryManaged")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
@@ -634,7 +635,7 @@ export default function ArticleFormPage() {
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Articolo Attivo"
|
||||
label={t("warehouse.articleForm.fields.active")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -643,7 +644,7 @@ export default function ArticleFormPage() {
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Note"
|
||||
label={t("warehouse.articleForm.fields.notes")}
|
||||
value={formData.notes}
|
||||
onChange={(e) => handleChange("notes", e.target.value)}
|
||||
multiline
|
||||
@@ -656,14 +657,14 @@ export default function ArticleFormPage() {
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Immagine
|
||||
{t("warehouse.articleForm.sections.image")}
|
||||
</Typography>
|
||||
{imagePreview ? (
|
||||
<Card sx={{ mb: 2 }}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={imagePreview}
|
||||
alt="Anteprima"
|
||||
alt={t("common.preview")}
|
||||
sx={{
|
||||
height: 200,
|
||||
objectFit: "contain",
|
||||
@@ -693,7 +694,7 @@ export default function ArticleFormPage() {
|
||||
startIcon={<UploadIcon />}
|
||||
fullWidth
|
||||
>
|
||||
Carica
|
||||
{t("warehouse.articleForm.actions.upload")}
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
@@ -713,7 +714,7 @@ export default function ArticleFormPage() {
|
||||
{!isNew && article && (
|
||||
<Paper sx={{ p: 3, mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Riepilogo
|
||||
{t("warehouse.articleForm.sections.summary")}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
|
||||
@@ -722,7 +723,7 @@ export default function ArticleFormPage() {
|
||||
sx={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<Typography color="text.secondary">
|
||||
Costo Medio:
|
||||
{t("warehouse.articleForm.summary.averageCost")}:
|
||||
</Typography>
|
||||
<Typography fontWeight="medium">
|
||||
{formatCurrency(article.weightedAverageCost || 0)}
|
||||
@@ -732,7 +733,7 @@ export default function ArticleFormPage() {
|
||||
sx={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<Typography color="text.secondary">
|
||||
Ultimo Acquisto:
|
||||
{t("warehouse.articleForm.summary.lastPurchase")}:
|
||||
</Typography>
|
||||
<Typography fontWeight="medium">
|
||||
{formatCurrency(article.lastPurchaseCost || 0)}
|
||||
@@ -746,7 +747,7 @@ export default function ArticleFormPage() {
|
||||
{/* Submit Button */}
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
|
||||
<Button onClick={() => navigate(-1)}>Annulla</Button>
|
||||
<Button onClick={() => navigate(-1)}>{t("warehouse.articleForm.actions.cancel")}</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -755,7 +756,7 @@ export default function ArticleFormPage() {
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? "Salvataggio..." : "Salva"}
|
||||
{isPending ? t("warehouse.articleForm.actions.saving") : t("warehouse.articleForm.actions.save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -767,17 +768,17 @@ export default function ArticleFormPage() {
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Giacenze per Magazzino
|
||||
{t("warehouse.articleForm.sections.stockLevels")}
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Magazzino</TableCell>
|
||||
<TableCell align="right">Quantità</TableCell>
|
||||
<TableCell align="right">Riservata</TableCell>
|
||||
<TableCell align="right">Disponibile</TableCell>
|
||||
<TableCell align="right">Valore</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.warehouse")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.articleForm.tables.quantity")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.articleForm.tables.reserved")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.articleForm.tables.available")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.articleForm.tables.value")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -785,7 +786,7 @@ export default function ArticleFormPage() {
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} align="center">
|
||||
<Typography color="text.secondary">
|
||||
Nessuna giacenza
|
||||
{t("warehouse.articleForm.tables.noStock")}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -822,16 +823,16 @@ export default function ArticleFormPage() {
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Lotti
|
||||
{t("warehouse.articleForm.tabs.batches")}
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Numero Lotto</TableCell>
|
||||
<TableCell align="right">Quantità</TableCell>
|
||||
<TableCell>Data Scadenza</TableCell>
|
||||
<TableCell>Stato</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.batchNumber")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.articleForm.tables.quantity")}</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.expiryDate")}</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.status")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -839,7 +840,7 @@ export default function ArticleFormPage() {
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center">
|
||||
<Typography color="text.secondary">
|
||||
Nessun lotto
|
||||
{t("warehouse.articleForm.tables.noBatches")}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -858,7 +859,7 @@ export default function ArticleFormPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={batch.isExpired ? "Scaduto" : "Disponibile"}
|
||||
label={batch.isExpired ? t("warehouse.articleForm.status.expired") : t("warehouse.articleForm.status.available")}
|
||||
size="small"
|
||||
color={batch.isExpired ? "error" : "success"}
|
||||
/>
|
||||
@@ -878,16 +879,16 @@ export default function ArticleFormPage() {
|
||||
<TabPanel value={tabValue} index={article?.isBatchManaged ? 3 : 2}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Matricole
|
||||
{t("warehouse.articleForm.tabs.serials")}
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Matricola</TableCell>
|
||||
<TableCell>Magazzino</TableCell>
|
||||
<TableCell>Lotto</TableCell>
|
||||
<TableCell>Stato</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.serialNumber")}</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.warehouse")}</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.lot")}</TableCell>
|
||||
<TableCell>{t("warehouse.articleForm.tables.status")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -895,7 +896,7 @@ export default function ArticleFormPage() {
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} align="center">
|
||||
<Typography color="text.secondary">
|
||||
Nessuna matricola
|
||||
{t("warehouse.articleForm.tables.noSerials")}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -911,8 +912,8 @@ export default function ArticleFormPage() {
|
||||
<Chip
|
||||
label={
|
||||
serial.status === 0
|
||||
? "Disponibile"
|
||||
: "Non disponibile"
|
||||
? t("warehouse.articleForm.status.available")
|
||||
: t("warehouse.articleForm.status.unavailable")
|
||||
}
|
||||
size="small"
|
||||
color={serial.status === 0 ? "success" : "default"}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
FilterList as FilterListIcon,
|
||||
Image as ImageIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { useArticles, useDeleteArticle, useCategoryTree } from "../hooks";
|
||||
import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation";
|
||||
@@ -51,6 +52,7 @@ import { ArticleDto, formatCurrency } from "../types";
|
||||
type ViewMode = "list" | "grid";
|
||||
|
||||
export default function ArticlesPage() {
|
||||
const { t } = useTranslation();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||
const [search, setSearch] = useState("");
|
||||
const [categoryId, setCategoryId] = useState<number | "">("");
|
||||
@@ -167,7 +169,7 @@ export default function ArticlesPage() {
|
||||
},
|
||||
{
|
||||
field: "code",
|
||||
headerName: "Codice",
|
||||
headerName: t("warehouse.articles.columns.code"),
|
||||
width: 120,
|
||||
renderCell: (params: GridRenderCellParams<ArticleDto>) => (
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
@@ -177,24 +179,24 @@ export default function ArticlesPage() {
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
headerName: "Descrizione",
|
||||
headerName: t("warehouse.articles.columns.description"),
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: "categoryName",
|
||||
headerName: "Categoria",
|
||||
headerName: t("warehouse.articles.columns.category"),
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: "unitOfMeasure",
|
||||
headerName: "U.M.",
|
||||
headerName: t("warehouse.articles.columns.uom"),
|
||||
width: 80,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
field: "weightedAverageCost",
|
||||
headerName: "Costo Medio",
|
||||
headerName: t("warehouse.articles.columns.averageCost"),
|
||||
width: 120,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<ArticleDto>) =>
|
||||
@@ -202,11 +204,11 @@ export default function ArticlesPage() {
|
||||
},
|
||||
{
|
||||
field: "isActive",
|
||||
headerName: "Stato",
|
||||
headerName: t("warehouse.articles.columns.status"),
|
||||
width: 100,
|
||||
renderCell: (params: GridRenderCellParams<ArticleDto>) => (
|
||||
<Chip
|
||||
label={params.value ? "Attivo" : "Inattivo"}
|
||||
label={params.value ? t("warehouse.articles.columns.active") : t("warehouse.articles.columns.inactive")}
|
||||
size="small"
|
||||
color={params.value ? "success" : "default"}
|
||||
/>
|
||||
@@ -229,7 +231,7 @@ export default function ArticlesPage() {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Alert severity="error">
|
||||
Errore nel caricamento degli articoli: {(error as Error).message}
|
||||
{t("warehouse.articles.loadingError", { error: (error as Error).message })}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
@@ -247,14 +249,14 @@ export default function ArticlesPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Anagrafica Articoli
|
||||
{t("warehouse.articles.title")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={nav.goToNewArticle}
|
||||
>
|
||||
Nuovo Articolo
|
||||
{t("warehouse.articles.newArticle")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -265,7 +267,7 @@ export default function ArticlesPage() {
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Cerca per codice o descrizione..."
|
||||
placeholder={t("warehouse.articles.filters.searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
InputProps={{
|
||||
@@ -286,14 +288,14 @@ export default function ArticlesPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<InputLabel>{t("warehouse.articles.filters.category")}</InputLabel>
|
||||
<Select
|
||||
value={categoryId}
|
||||
label="Categoria"
|
||||
label={t("warehouse.articles.filters.category")}
|
||||
onChange={(e) => setCategoryId(e.target.value as number | "")}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutte</em>
|
||||
<em>{t("warehouse.articles.filters.all")}</em>
|
||||
</MenuItem>
|
||||
{flatCategories.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
@@ -311,7 +313,7 @@ export default function ArticlesPage() {
|
||||
onClick={() => setShowInactive(!showInactive)}
|
||||
fullWidth
|
||||
>
|
||||
{showInactive ? "Mostra Tutti" : "Solo Attivi"}
|
||||
{showInactive ? t("warehouse.articles.filters.showAll") : t("warehouse.articles.filters.onlyActive")}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid
|
||||
@@ -325,12 +327,12 @@ export default function ArticlesPage() {
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="list">
|
||||
<Tooltip title="Vista Lista">
|
||||
<Tooltip title={t("warehouse.articles.filters.viewList")}>
|
||||
<ViewListIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="grid">
|
||||
<Tooltip title="Vista Griglia">
|
||||
<Tooltip title={t("warehouse.articles.filters.viewGrid")}>
|
||||
<ViewModuleIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
@@ -381,7 +383,7 @@ export default function ArticlesPage() {
|
||||
sx={{ fontSize: 48, color: "grey.400", mb: 2 }}
|
||||
/>
|
||||
<Typography color="text.secondary">
|
||||
Nessun articolo trovato
|
||||
{t("warehouse.articles.noArticlesFound")}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
@@ -445,7 +447,7 @@ export default function ArticlesPage() {
|
||||
nav.goToEditArticle(article.id);
|
||||
}}
|
||||
>
|
||||
Modifica
|
||||
{t("warehouse.articles.actions.edit")}
|
||||
</Button>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -472,19 +474,19 @@ export default function ArticlesPage() {
|
||||
<ListItemIcon>
|
||||
<EditIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Modifica</ListItemText>
|
||||
<ListItemText>{t("warehouse.articles.actions.edit")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleViewStock}>
|
||||
<ListItemIcon>
|
||||
<InventoryIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Visualizza Giacenze</ListItemText>
|
||||
<ListItemText>{t("warehouse.articles.actions.viewStock")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDeleteClick} sx={{ color: "error.main" }}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon fontSize="small" color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Elimina</ListItemText>
|
||||
<ListItemText>{t("warehouse.articles.actions.delete")}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -493,28 +495,28 @@ export default function ArticlesPage() {
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Conferma Eliminazione</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.articles.deleteDialog.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler eliminare l'articolo{" "}
|
||||
<strong>
|
||||
{articleToDelete?.code} - {articleToDelete?.description}
|
||||
</strong>
|
||||
?
|
||||
<Trans
|
||||
i18nKey="warehouse.articles.deleteDialog.content"
|
||||
values={{ code: articleToDelete?.code, description: articleToDelete?.description }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Questa azione non può essere annullata.
|
||||
{t("warehouse.articles.deleteDialog.warning")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>{t("warehouse.articles.deleteDialog.cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleDeleteConfirm}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
|
||||
{deleteMutation.isPending ? t("warehouse.articles.deleteDialog.deleting") : t("warehouse.articles.deleteDialog.delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
Delete as DeleteIcon,
|
||||
Check as ConfirmIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@@ -54,6 +55,7 @@ interface MovementLine {
|
||||
}
|
||||
|
||||
export default function InboundMovementPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [movementDate, setMovementDate] = useState<Dayjs | null>(dayjs());
|
||||
const [warehouseId, setWarehouseId] = useState<number | "">("");
|
||||
@@ -124,14 +126,14 @@ export default function InboundMovementPage() {
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!warehouseId) {
|
||||
newErrors.warehouseId = "Seleziona un magazzino";
|
||||
newErrors.warehouseId = t("warehouse.inbound.validation.warehouseRequired");
|
||||
}
|
||||
if (!movementDate) {
|
||||
newErrors.movementDate = "Inserisci la data";
|
||||
newErrors.movementDate = t("warehouse.inbound.validation.dateRequired");
|
||||
}
|
||||
const validLines = lines.filter((l) => l.article && l.quantity > 0);
|
||||
if (validLines.length === 0) {
|
||||
newErrors.lines = "Inserisci almeno una riga con articolo e quantità";
|
||||
newErrors.lines = t("warehouse.inbound.validation.linesRequired");
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -179,30 +181,29 @@ export default function InboundMovementPage() {
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Nuovo Carico
|
||||
{t("warehouse.inbound.title")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Movimento di entrata merce in magazzino
|
||||
{t("warehouse.inbound.subtitle")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{(createMutation.error || confirmMutation.error) && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
Errore:{" "}
|
||||
{((createMutation.error || confirmMutation.error) as Error).message}
|
||||
{t("warehouse.inbound.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Form Header */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Dati Movimento
|
||||
{t("warehouse.inbound.sections.movementData")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<DatePicker
|
||||
label="Data Movimento"
|
||||
label={t("warehouse.inbound.fields.date")}
|
||||
value={movementDate}
|
||||
onChange={setMovementDate}
|
||||
slotProps={{
|
||||
@@ -217,10 +218,10 @@ export default function InboundMovementPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth required error={!!errors.warehouseId}>
|
||||
<InputLabel>Magazzino</InputLabel>
|
||||
<InputLabel>{t("warehouse.inbound.fields.warehouse")}</InputLabel>
|
||||
<Select
|
||||
value={warehouseId}
|
||||
label="Magazzino"
|
||||
label={t("warehouse.inbound.fields.warehouse")}
|
||||
onChange={(e) => setWarehouseId(e.target.value as number)}
|
||||
>
|
||||
{warehouses?.map((w) => (
|
||||
@@ -242,25 +243,25 @@ export default function InboundMovementPage() {
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Numero Documento"
|
||||
label={t("warehouse.inbound.fields.documentNumber")}
|
||||
value={documentNumber}
|
||||
onChange={(e) => setDocumentNumber(e.target.value)}
|
||||
placeholder="DDT, Fattura, etc."
|
||||
placeholder={t("warehouse.inbound.placeholders.documentNumber")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Riferimento Esterno"
|
||||
label={t("warehouse.inbound.fields.externalReference")}
|
||||
value={externalReference}
|
||||
onChange={(e) => setExternalReference(e.target.value)}
|
||||
placeholder="Ordine, Fornitore, etc."
|
||||
placeholder={t("warehouse.inbound.placeholders.externalReference")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Note"
|
||||
label={t("warehouse.inbound.fields.notes")}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
multiline
|
||||
@@ -280,9 +281,9 @@ export default function InboundMovementPage() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Righe Movimento</Typography>
|
||||
<Typography variant="h6">{t("warehouse.inbound.sections.lines")}</Typography>
|
||||
<Button startIcon={<AddIcon />} onClick={handleAddLine}>
|
||||
Aggiungi Riga
|
||||
{t("warehouse.inbound.actions.addLine")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -296,15 +297,15 @@ export default function InboundMovementPage() {
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: "45%" }}>Articolo</TableCell>
|
||||
<TableCell sx={{ width: "45%" }}>{t("warehouse.inbound.fields.article")}</TableCell>
|
||||
<TableCell sx={{ width: "15%" }} align="right">
|
||||
Quantità
|
||||
{t("warehouse.inbound.fields.quantity")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "15%" }} align="right">
|
||||
Costo Unitario
|
||||
{t("warehouse.inbound.fields.unitCost")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "15%" }} align="right">
|
||||
Totale
|
||||
{t("warehouse.inbound.fields.total")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: 60 }}></TableCell>
|
||||
</TableRow>
|
||||
@@ -326,7 +327,7 @@ export default function InboundMovementPage() {
|
||||
<TextField
|
||||
{...params}
|
||||
size="small"
|
||||
placeholder="Seleziona articolo"
|
||||
placeholder={t("warehouse.inbound.placeholders.selectArticle")}
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
@@ -410,13 +411,13 @@ export default function InboundMovementPage() {
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Totale Quantità
|
||||
{t("warehouse.inbound.totals.quantity")}
|
||||
</Typography>
|
||||
<Typography variant="h6">{totalQuantity.toFixed(2)}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Totale Valore
|
||||
{t("warehouse.inbound.totals.value")}
|
||||
</Typography>
|
||||
<Typography variant="h6">{formatCurrency(totalValue)}</Typography>
|
||||
</Box>
|
||||
@@ -425,7 +426,7 @@ export default function InboundMovementPage() {
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
|
||||
<Button onClick={() => navigate(-1)}>Annulla</Button>
|
||||
<Button onClick={() => navigate(-1)}>{t("warehouse.inbound.actions.cancel")}</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={
|
||||
@@ -434,7 +435,7 @@ export default function InboundMovementPage() {
|
||||
onClick={() => handleSubmit(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Salva Bozza
|
||||
{t("warehouse.inbound.actions.saveDraft")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -445,7 +446,7 @@ export default function InboundMovementPage() {
|
||||
disabled={isPending}
|
||||
color="success"
|
||||
>
|
||||
Salva e Conferma
|
||||
{t("warehouse.inbound.actions.saveAndConfirm")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -31,11 +31,13 @@ import {
|
||||
DoneAll as ConfirmIcon,
|
||||
ArrowBack as ArrowBackIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { inventoryService } from "../services/warehouseService";
|
||||
import { InventoryStatus, InventoryCountLineDto } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function InventoryCountPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -107,25 +109,25 @@ export default function InventoryCountPage() {
|
||||
const isEditable = inventory.status === InventoryStatus.InProgress;
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: "articleCode", headerName: "Codice Articolo", width: 150 },
|
||||
{ field: "articleCode", headerName: t("warehouse.inventory.count.columns.articleCode"), width: 150 },
|
||||
{
|
||||
field: "articleDescription",
|
||||
headerName: "Descrizione",
|
||||
headerName: t("warehouse.inventory.count.columns.description"),
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
},
|
||||
{ field: "batchNumber", headerName: "Lotto", width: 120 },
|
||||
{ field: "locationCode", headerName: "Ubicazione", width: 120 },
|
||||
{ field: "batchNumber", headerName: t("warehouse.inventory.count.columns.batch"), width: 120 },
|
||||
{ field: "locationCode", headerName: t("warehouse.inventory.count.columns.location"), width: 120 },
|
||||
{
|
||||
field: "theoreticalQuantity",
|
||||
headerName: "Qta Teorica",
|
||||
headerName: t("warehouse.inventory.count.columns.theoreticalQty"),
|
||||
width: 120,
|
||||
type: "number",
|
||||
valueFormatter: (value) => (value ? Number(value).toFixed(2) : "0"),
|
||||
},
|
||||
{
|
||||
field: "countedQuantity",
|
||||
headerName: "Qta Contata",
|
||||
headerName: t("warehouse.inventory.count.columns.countedQty"),
|
||||
width: 150,
|
||||
type: "number",
|
||||
editable: isEditable,
|
||||
@@ -137,7 +139,7 @@ export default function InventoryCountPage() {
|
||||
},
|
||||
{
|
||||
field: "difference",
|
||||
headerName: "Differenza",
|
||||
headerName: t("warehouse.inventory.count.columns.difference"),
|
||||
width: 120,
|
||||
type: "number",
|
||||
valueGetter: (_value, row) => {
|
||||
@@ -179,10 +181,10 @@ export default function InventoryCountPage() {
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate("/warehouse/inventory")}
|
||||
>
|
||||
Indietro
|
||||
{t("warehouse.inventory.count.actions.back")}
|
||||
</Button>
|
||||
<Typography variant="h4">
|
||||
Inventario: {inventory.description}
|
||||
{t("warehouse.inventory.count.title", { description: inventory.description })}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={inventory.status}
|
||||
@@ -203,7 +205,7 @@ export default function InventoryCountPage() {
|
||||
startIcon={<StartIcon />}
|
||||
onClick={() => startMutation.mutate()}
|
||||
>
|
||||
Avvia Inventario
|
||||
{t("warehouse.inventory.count.actions.start")}
|
||||
</Button>
|
||||
)}
|
||||
{inventory.status === InventoryStatus.InProgress && (
|
||||
@@ -213,7 +215,7 @@ export default function InventoryCountPage() {
|
||||
startIcon={<CompleteIcon />}
|
||||
onClick={() => completeMutation.mutate()}
|
||||
>
|
||||
Completa Conteggio
|
||||
{t("warehouse.inventory.count.actions.complete")}
|
||||
</Button>
|
||||
)}
|
||||
{inventory.status === InventoryStatus.Completed && (
|
||||
@@ -223,7 +225,7 @@ export default function InventoryCountPage() {
|
||||
startIcon={<ConfirmIcon />}
|
||||
onClick={() => setConfirmDialogOpen(true)}
|
||||
>
|
||||
Conferma e Rettifica
|
||||
{t("warehouse.inventory.count.actions.confirm")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -234,7 +236,7 @@ export default function InventoryCountPage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Data Inventario
|
||||
{t("warehouse.inventory.count.cards.date")}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{dayjs(inventory.inventoryDate).format("DD/MM/YYYY")}
|
||||
@@ -246,7 +248,7 @@ export default function InventoryCountPage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Magazzino
|
||||
{t("warehouse.inventory.count.cards.warehouse")}
|
||||
</Typography>
|
||||
<Typography variant="h6">{inventory.warehouseName}</Typography>
|
||||
</CardContent>
|
||||
@@ -256,7 +258,7 @@ export default function InventoryCountPage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Righe Totali
|
||||
{t("warehouse.inventory.count.cards.totalLines")}
|
||||
</Typography>
|
||||
<Typography variant="h6">{inventory.lineCount}</Typography>
|
||||
</CardContent>
|
||||
@@ -266,7 +268,7 @@ export default function InventoryCountPage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Righe Contate
|
||||
{t("warehouse.inventory.count.cards.countedLines")}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{inventory.lines.filter((l) => l.countedQuantity !== null).length}
|
||||
@@ -278,8 +280,7 @@ export default function InventoryCountPage() {
|
||||
|
||||
{inventory.status === InventoryStatus.Completed && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
L'inventario è completato. Verifica le differenze prima di confermare.
|
||||
La conferma genererà automaticamente i movimenti di rettifica.
|
||||
{t("warehouse.inventory.count.alert.completed")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -308,23 +309,21 @@ export default function InventoryCountPage() {
|
||||
open={confirmDialogOpen}
|
||||
onClose={() => setConfirmDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Conferma Inventario</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.inventory.count.confirmDialog.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Sei sicuro di voler confermare l'inventario? Questa operazione è
|
||||
irreversibile e genererà i movimenti di rettifica per le differenze
|
||||
riscontrate.
|
||||
{t("warehouse.inventory.count.confirmDialog.content")}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={() => setConfirmDialogOpen(false)}>{t("warehouse.inventory.count.confirmDialog.cancel")}</Button>
|
||||
<Button
|
||||
onClick={() => confirmMutation.mutate()}
|
||||
color="success"
|
||||
variant="contained"
|
||||
autoFocus
|
||||
>
|
||||
Conferma
|
||||
{t("warehouse.inventory.count.confirmDialog.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { Save as SaveIcon, ArrowBack as ArrowBackIcon } from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
inventoryService,
|
||||
warehouseLocationService,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function InventoryFormPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -111,7 +113,7 @@ export default function InventoryFormPage() {
|
||||
Inventari
|
||||
</Link>
|
||||
<Typography color="text.primary">
|
||||
{isEditing ? "Modifica Inventario" : "Nuovo Inventario"}
|
||||
{isEditing ? t("warehouse.inventory.form.title.edit") : t("warehouse.inventory.form.title.new")}
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
|
||||
@@ -124,13 +126,13 @@ export default function InventoryFormPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">
|
||||
{isEditing ? `Inventario ${inventory?.code}` : "Nuovo Inventario"}
|
||||
{isEditing ? t("warehouse.inventory.form.title.editWithCode", { code: inventory?.code }) : t("warehouse.inventory.form.title.new")}
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate("/warehouse/inventory")}
|
||||
>
|
||||
Indietro
|
||||
{t("warehouse.inventory.form.actions.back")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -139,7 +141,7 @@ export default function InventoryFormPage() {
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
label={t("warehouse.inventory.form.fields.description")}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.description}
|
||||
@@ -150,7 +152,7 @@ export default function InventoryFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Data Inventario"
|
||||
label={t("warehouse.inventory.form.fields.date")}
|
||||
type="date"
|
||||
fullWidth
|
||||
required
|
||||
@@ -163,10 +165,10 @@ export default function InventoryFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Magazzino</InputLabel>
|
||||
<InputLabel>{t("warehouse.inventory.form.fields.warehouse")}</InputLabel>
|
||||
<Select
|
||||
value={formData.warehouseId || ""}
|
||||
label="Magazzino"
|
||||
label={t("warehouse.inventory.form.fields.warehouse")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -185,10 +187,10 @@ export default function InventoryFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Categoria (Opzionale)</InputLabel>
|
||||
<InputLabel>{t("warehouse.inventory.form.fields.category")}</InputLabel>
|
||||
<Select
|
||||
value={formData.categoryId || ""}
|
||||
label="Categoria (Opzionale)"
|
||||
label={t("warehouse.inventory.form.fields.category")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -197,7 +199,7 @@ export default function InventoryFormPage() {
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutte</em>
|
||||
<em>{t("warehouse.inventory.form.options.allCategories")}</em>
|
||||
</MenuItem>
|
||||
{categories.map((c) => (
|
||||
<MenuItem key={c.id} value={c.id}>
|
||||
@@ -209,10 +211,10 @@ export default function InventoryFormPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Tipo Inventario</InputLabel>
|
||||
<InputLabel>{t("warehouse.inventory.form.fields.type")}</InputLabel>
|
||||
<Select
|
||||
value={formData.type}
|
||||
label="Tipo Inventario"
|
||||
label={t("warehouse.inventory.form.fields.type")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -220,16 +222,16 @@ export default function InventoryFormPage() {
|
||||
})
|
||||
}
|
||||
>
|
||||
<MenuItem value={InventoryType.Full}>Completo</MenuItem>
|
||||
<MenuItem value={InventoryType.Partial}>Parziale</MenuItem>
|
||||
<MenuItem value={InventoryType.Cyclic}>Ciclico</MenuItem>
|
||||
<MenuItem value={InventoryType.Sample}>A Campione</MenuItem>
|
||||
<MenuItem value={InventoryType.Full}>{t("warehouse.inventory.form.options.type.Full")}</MenuItem>
|
||||
<MenuItem value={InventoryType.Partial}>{t("warehouse.inventory.form.options.type.Partial")}</MenuItem>
|
||||
<MenuItem value={InventoryType.Cyclic}>{t("warehouse.inventory.form.options.type.Cyclic")}</MenuItem>
|
||||
<MenuItem value={InventoryType.Sample}>{t("warehouse.inventory.form.options.type.Sample")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("warehouse.inventory.form.fields.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
@@ -247,7 +249,7 @@ export default function InventoryFormPage() {
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
{isEditing ? "Salva Modifiche" : "Crea e Inizia"}
|
||||
{isEditing ? t("warehouse.inventory.form.actions.save") : t("warehouse.inventory.form.actions.create")}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -22,11 +22,13 @@ import {
|
||||
PlayArrow as StartIcon,
|
||||
Cancel as CancelIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { inventoryService } from "../services/warehouseService";
|
||||
import { InventoryCountDto, InventoryStatus } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function InventoryListPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [statusFilter] = useState<InventoryStatus | undefined>(
|
||||
@@ -58,44 +60,45 @@ export default function InventoryListPage() {
|
||||
};
|
||||
|
||||
const getStatusChip = (status: InventoryStatus) => {
|
||||
const label = t(`warehouse.inventory.status.${InventoryStatus[status]}`);
|
||||
switch (status) {
|
||||
case InventoryStatus.Draft:
|
||||
return <Chip label="Bozza" size="small" />;
|
||||
return <Chip label={label} size="small" />;
|
||||
case InventoryStatus.InProgress:
|
||||
return <Chip label="In Corso" color="primary" size="small" />;
|
||||
return <Chip label={label} color="primary" size="small" />;
|
||||
case InventoryStatus.Completed:
|
||||
return <Chip label="Completato" color="info" size="small" />;
|
||||
return <Chip label={label} color="info" size="small" />;
|
||||
case InventoryStatus.Confirmed:
|
||||
return <Chip label="Confermato" color="success" size="small" />;
|
||||
return <Chip label={label} color="success" size="small" />;
|
||||
case InventoryStatus.Cancelled:
|
||||
return <Chip label="Annullato" color="error" size="small" />;
|
||||
return <Chip label={label} color="error" size="small" />;
|
||||
default:
|
||||
return <Chip label={status} size="small" />;
|
||||
return <Chip label={label} size="small" />;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: "code", headerName: "Codice", width: 120 },
|
||||
{ field: "description", headerName: "Descrizione", flex: 1, minWidth: 200 },
|
||||
{ field: "code", headerName: t("warehouse.inventory.columns.code"), width: 120 },
|
||||
{ field: "description", headerName: t("warehouse.inventory.columns.description"), flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: "inventoryDate",
|
||||
headerName: "Data Inventario",
|
||||
headerName: t("warehouse.inventory.columns.date"),
|
||||
width: 150,
|
||||
valueFormatter: (value) =>
|
||||
value ? dayjs(value).format("DD/MM/YYYY") : "",
|
||||
},
|
||||
{ field: "warehouseName", headerName: "Magazzino", width: 180 },
|
||||
{ field: "categoryName", headerName: "Categoria", width: 150 },
|
||||
{ field: "warehouseName", headerName: t("warehouse.inventory.columns.warehouse"), width: 180 },
|
||||
{ field: "categoryName", headerName: t("warehouse.inventory.columns.category"), width: 150 },
|
||||
{
|
||||
field: "status",
|
||||
headerName: "Stato",
|
||||
headerName: t("warehouse.inventory.columns.status"),
|
||||
width: 120,
|
||||
renderCell: (params: GridRenderCellParams<InventoryCountDto>) =>
|
||||
getStatusChip(params.row.status),
|
||||
},
|
||||
{
|
||||
field: "progress",
|
||||
headerName: "Progresso",
|
||||
headerName: t("warehouse.inventory.columns.progress"),
|
||||
width: 150,
|
||||
valueGetter: (_value, row) => {
|
||||
if (!row.lineCount) return "0%";
|
||||
@@ -107,18 +110,18 @@ export default function InventoryListPage() {
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Azioni",
|
||||
headerName: t("warehouse.inventory.columns.actions"),
|
||||
width: 180,
|
||||
sortable: false,
|
||||
renderCell: (params: GridRenderCellParams<InventoryCountDto>) => (
|
||||
<Box>
|
||||
<Tooltip title="Dettaglio">
|
||||
<Tooltip title={t("warehouse.inventory.actions.view")}>
|
||||
<IconButton size="small" onClick={() => handleView(params.row.id)}>
|
||||
<ViewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{params.row.status === InventoryStatus.Draft && (
|
||||
<Tooltip title="Avvia Conteggio">
|
||||
<Tooltip title={t("warehouse.inventory.actions.start")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
@@ -129,7 +132,7 @@ export default function InventoryListPage() {
|
||||
</Tooltip>
|
||||
)}
|
||||
{params.row.status === InventoryStatus.InProgress && (
|
||||
<Tooltip title="Continua Conteggio">
|
||||
<Tooltip title={t("warehouse.inventory.actions.continue")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
@@ -140,12 +143,12 @@ export default function InventoryListPage() {
|
||||
</Tooltip>
|
||||
)}
|
||||
{params.row.status === InventoryStatus.Draft && (
|
||||
<Tooltip title="Annulla">
|
||||
<Tooltip title={t("warehouse.inventory.actions.cancel")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm("Sei sicuro di voler annullare questo inventario?")) {
|
||||
if (confirm(t("warehouse.inventory.confirmCancel"))) {
|
||||
cancelMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -169,13 +172,13 @@ export default function InventoryListPage() {
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Inventari Fisici</Typography>
|
||||
<Typography variant="h4">{t("warehouse.inventory.title")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
Nuovo Inventario
|
||||
{t("warehouse.inventory.newInventory")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
Build as AdjustmentIcon,
|
||||
FilterList as FilterIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
@@ -66,6 +67,7 @@ import {
|
||||
} from "../types";
|
||||
|
||||
export default function MovementsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [warehouseId, setWarehouseId] = useState<number | "">("");
|
||||
const [movementType, setMovementType] = useState<MovementType | "">("");
|
||||
@@ -183,7 +185,7 @@ export default function MovementsPage() {
|
||||
const columns: GridColDef<MovementDto>[] = [
|
||||
{
|
||||
field: "documentNumber",
|
||||
headerName: "Documento",
|
||||
headerName: t("warehouse.movements.columns.document"),
|
||||
width: 140,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) => (
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
@@ -193,44 +195,44 @@ export default function MovementsPage() {
|
||||
},
|
||||
{
|
||||
field: "movementDate",
|
||||
headerName: "Data",
|
||||
headerName: t("warehouse.movements.columns.date"),
|
||||
width: 110,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) =>
|
||||
formatDate(params.value),
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Tipo",
|
||||
headerName: t("warehouse.movements.columns.type"),
|
||||
width: 130,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) => (
|
||||
<Chip
|
||||
label={movementTypeLabels[params.value as MovementType]}
|
||||
label={t(`warehouse.movementType.${MovementType[params.value as MovementType]}`)}
|
||||
size="small"
|
||||
color={
|
||||
getMovementTypeColor(params.value as MovementType) as
|
||||
| "success"
|
||||
| "error"
|
||||
| "info"
|
||||
| "warning"
|
||||
| "default"
|
||||
| "success"
|
||||
| "error"
|
||||
| "info"
|
||||
| "warning"
|
||||
| "default"
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "status",
|
||||
headerName: "Stato",
|
||||
headerName: t("warehouse.movements.columns.status"),
|
||||
width: 120,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) => (
|
||||
<Chip
|
||||
label={movementStatusLabels[params.value as MovementStatus]}
|
||||
label={t(`warehouse.movementStatus.${MovementStatus[params.value as MovementStatus]}`)}
|
||||
size="small"
|
||||
color={
|
||||
getMovementStatusColor(params.value as MovementStatus) as
|
||||
| "success"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "default"
|
||||
| "success"
|
||||
| "error"
|
||||
| "warning"
|
||||
| "default"
|
||||
}
|
||||
variant={
|
||||
params.value === MovementStatus.Draft ? "outlined" : "filled"
|
||||
@@ -240,7 +242,7 @@ export default function MovementsPage() {
|
||||
},
|
||||
{
|
||||
field: "sourceWarehouseName",
|
||||
headerName: "Magazzino",
|
||||
headerName: t("warehouse.movements.columns.warehouse"),
|
||||
width: 150,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) =>
|
||||
params.row.sourceWarehouseName ||
|
||||
@@ -249,7 +251,7 @@ export default function MovementsPage() {
|
||||
},
|
||||
{
|
||||
field: "destinationWarehouseName",
|
||||
headerName: "Destinazione",
|
||||
headerName: t("warehouse.movements.columns.destination"),
|
||||
width: 150,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) => {
|
||||
// Show destination only for transfers
|
||||
@@ -261,33 +263,33 @@ export default function MovementsPage() {
|
||||
},
|
||||
{
|
||||
field: "reasonDescription",
|
||||
headerName: "Causale",
|
||||
headerName: t("warehouse.movements.columns.reason"),
|
||||
width: 150,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) =>
|
||||
params.value || "-",
|
||||
},
|
||||
{
|
||||
field: "lineCount",
|
||||
headerName: "Righe",
|
||||
headerName: t("warehouse.movements.columns.lines"),
|
||||
width: 80,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
field: "totalValue",
|
||||
headerName: "Valore",
|
||||
headerName: t("warehouse.movements.columns.value"),
|
||||
width: 100,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) =>
|
||||
params.value != null
|
||||
? new Intl.NumberFormat("it-IT", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(params.value)
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(params.value)
|
||||
: "-",
|
||||
},
|
||||
{
|
||||
field: "externalReference",
|
||||
headerName: "Riferimento",
|
||||
headerName: t("warehouse.movements.columns.reference"),
|
||||
width: 140,
|
||||
renderCell: (params: GridRenderCellParams<MovementDto>) =>
|
||||
params.value || "-",
|
||||
@@ -306,16 +308,16 @@ export default function MovementsPage() {
|
||||
];
|
||||
|
||||
const speedDialActions = [
|
||||
{ icon: <DownloadIcon />, name: "Carico", action: nav.goToNewInbound },
|
||||
{ icon: <UploadIcon />, name: "Scarico", action: nav.goToNewOutbound },
|
||||
{ icon: <DownloadIcon />, name: t("warehouse.movements.actions.inbound"), action: nav.goToNewInbound },
|
||||
{ icon: <UploadIcon />, name: t("warehouse.movements.actions.outbound"), action: nav.goToNewOutbound },
|
||||
{
|
||||
icon: <TransferIcon />,
|
||||
name: "Trasferimento",
|
||||
name: t("warehouse.movements.actions.transfer"),
|
||||
action: nav.goToNewTransfer,
|
||||
},
|
||||
{
|
||||
icon: <AdjustmentIcon />,
|
||||
name: "Rettifica",
|
||||
name: t("warehouse.movements.actions.adjustment"),
|
||||
action: nav.goToNewAdjustment,
|
||||
},
|
||||
];
|
||||
@@ -324,7 +326,7 @@ export default function MovementsPage() {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Alert severity="error">
|
||||
Errore nel caricamento dei movimenti: {(error as Error).message}
|
||||
{t("warehouse.movements.loadingError", { error: (error as Error).message })}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
@@ -343,7 +345,7 @@ export default function MovementsPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Movimenti di Magazzino
|
||||
{t("warehouse.movements.title")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -354,7 +356,7 @@ export default function MovementsPage() {
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Cerca documento, riferimento..."
|
||||
placeholder={t("warehouse.movements.filters.searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
slotProps={{
|
||||
@@ -377,16 +379,16 @@ export default function MovementsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Magazzino</InputLabel>
|
||||
<InputLabel>{t("warehouse.movements.filters.warehouse")}</InputLabel>
|
||||
<Select
|
||||
value={warehouseId}
|
||||
label="Magazzino"
|
||||
label={t("warehouse.movements.filters.warehouse")}
|
||||
onChange={(e) =>
|
||||
setWarehouseId(e.target.value as number | "")
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutti</em>
|
||||
<em>{t("warehouse.movements.filters.all")}</em>
|
||||
</MenuItem>
|
||||
{warehouses?.map((w) => (
|
||||
<MenuItem key={w.id} value={w.id}>
|
||||
@@ -398,20 +400,20 @@ export default function MovementsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 4, md: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Tipo</InputLabel>
|
||||
<InputLabel>{t("warehouse.movements.filters.type")}</InputLabel>
|
||||
<Select
|
||||
value={movementType}
|
||||
label="Tipo"
|
||||
label={t("warehouse.movements.filters.type")}
|
||||
onChange={(e) =>
|
||||
setMovementType(e.target.value as MovementType | "")
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutti</em>
|
||||
<em>{t("warehouse.movements.filters.all")}</em>
|
||||
</MenuItem>
|
||||
{Object.entries(movementTypeLabels).map(([value, label]) => (
|
||||
{Object.entries(movementTypeLabels).map(([value]) => (
|
||||
<MenuItem key={value} value={parseInt(value, 10)}>
|
||||
{label}
|
||||
{t(`warehouse.movementType.${MovementType[parseInt(value, 10)]}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -419,21 +421,21 @@ export default function MovementsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 4, md: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Stato</InputLabel>
|
||||
<InputLabel>{t("warehouse.movements.filters.status")}</InputLabel>
|
||||
<Select
|
||||
value={status}
|
||||
label="Stato"
|
||||
label={t("warehouse.movements.filters.status")}
|
||||
onChange={(e) =>
|
||||
setStatus(e.target.value as MovementStatus | "")
|
||||
}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutti</em>
|
||||
<em>{t("warehouse.movements.filters.all")}</em>
|
||||
</MenuItem>
|
||||
{Object.entries(movementStatusLabels).map(
|
||||
([value, label]) => (
|
||||
([value]) => (
|
||||
<MenuItem key={value} value={parseInt(value, 10)}>
|
||||
{label}
|
||||
{t(`warehouse.movementStatus.${MovementStatus[parseInt(value, 10)]}`)}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
@@ -442,7 +444,7 @@ export default function MovementsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 4, md: 1.5 }}>
|
||||
<DatePicker
|
||||
label="Da"
|
||||
label={t("warehouse.movements.filters.from")}
|
||||
value={dateFrom}
|
||||
onChange={setDateFrom}
|
||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||
@@ -450,7 +452,7 @@ export default function MovementsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 4, md: 1.5 }}>
|
||||
<DatePicker
|
||||
label="A"
|
||||
label={t("warehouse.movements.filters.to")}
|
||||
value={dateTo}
|
||||
onChange={setDateTo}
|
||||
slotProps={{ textField: { size: "small", fullWidth: true } }}
|
||||
@@ -462,16 +464,16 @@ export default function MovementsPage() {
|
||||
status !== "" ||
|
||||
dateFrom ||
|
||||
dateTo) && (
|
||||
<Grid size="auto">
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<FilterIcon />}
|
||||
onClick={clearFilters}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid size="auto">
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<FilterIcon />}
|
||||
onClick={clearFilters}
|
||||
>
|
||||
{t("warehouse.movements.filters.reset")}
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
||||
@@ -498,7 +500,7 @@ export default function MovementsPage() {
|
||||
|
||||
{/* Speed Dial for New Movements */}
|
||||
<SpeedDial
|
||||
ariaLabel="Nuovo Movimento"
|
||||
ariaLabel={t("warehouse.movements.actions.newMovement")}
|
||||
sx={{ position: "fixed", bottom: 24, right: 24 }}
|
||||
icon={<SpeedDialIcon openIcon={<AddIcon />} />}
|
||||
open={speedDialOpen}
|
||||
@@ -529,7 +531,7 @@ export default function MovementsPage() {
|
||||
<ListItemIcon>
|
||||
<ViewIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Visualizza</ListItemText>
|
||||
<ListItemText>{t("warehouse.movements.actions.view")}</ListItemText>
|
||||
</MenuItem>
|
||||
{menuMovement?.status === MovementStatus.Draft && (
|
||||
<>
|
||||
@@ -537,13 +539,13 @@ export default function MovementsPage() {
|
||||
<ListItemIcon>
|
||||
<ConfirmIcon fontSize="small" color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Conferma</ListItemText>
|
||||
<ListItemText>{t("warehouse.movements.actions.confirm")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCancelClick}>
|
||||
<ListItemIcon>
|
||||
<CancelIcon fontSize="small" color="warning" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Annulla</ListItemText>
|
||||
<ListItemText>{t("warehouse.movements.actions.cancel")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleDeleteClick}
|
||||
@@ -552,7 +554,7 @@ export default function MovementsPage() {
|
||||
<ListItemIcon>
|
||||
<DeleteIcon fontSize="small" color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Elimina</ListItemText>
|
||||
<ListItemText>{t("warehouse.movements.actions.delete")}</ListItemText>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -563,26 +565,28 @@ export default function MovementsPage() {
|
||||
open={confirmDialogOpen}
|
||||
onClose={() => setConfirmDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Conferma Movimento</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.movements.dialogs.confirm.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Confermare il movimento{" "}
|
||||
<strong>{menuMovement?.documentNumber}</strong>?
|
||||
<Trans
|
||||
i18nKey="warehouse.movements.dialogs.confirm.content"
|
||||
values={{ doc: menuMovement?.documentNumber }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Le giacenze verranno aggiornate e il movimento non potrà più
|
||||
essere modificato.
|
||||
{t("warehouse.movements.dialogs.confirm.warning")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={() => setConfirmDialogOpen(false)}>{t("warehouse.movements.dialogs.confirm.cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleConfirmMovement}
|
||||
color="success"
|
||||
variant="contained"
|
||||
disabled={confirmMutation.isPending}
|
||||
>
|
||||
{confirmMutation.isPending ? "Conferma..." : "Conferma"}
|
||||
{confirmMutation.isPending ? t("warehouse.movements.dialogs.confirm.confirming") : t("warehouse.movements.dialogs.confirm.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -592,18 +596,21 @@ export default function MovementsPage() {
|
||||
open={cancelDialogOpen}
|
||||
onClose={() => setCancelDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Annulla Movimento</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.movements.dialogs.cancel.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Annullare il movimento{" "}
|
||||
<strong>{menuMovement?.documentNumber}</strong>?
|
||||
<Trans
|
||||
i18nKey="warehouse.movements.dialogs.cancel.content"
|
||||
values={{ doc: menuMovement?.documentNumber }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Il movimento verrà marcato come annullato ma non eliminato.
|
||||
{t("warehouse.movements.dialogs.cancel.warning")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCancelDialogOpen(false)}>Indietro</Button>
|
||||
<Button onClick={() => setCancelDialogOpen(false)}>{t("warehouse.movements.dialogs.cancel.back")}</Button>
|
||||
<Button
|
||||
onClick={handleCancelMovement}
|
||||
color="warning"
|
||||
@@ -611,8 +618,8 @@ export default function MovementsPage() {
|
||||
disabled={cancelMutation.isPending}
|
||||
>
|
||||
{cancelMutation.isPending
|
||||
? "Annullamento..."
|
||||
: "Annulla Movimento"}
|
||||
? t("warehouse.movements.dialogs.cancel.cancelling")
|
||||
: t("warehouse.movements.dialogs.cancel.cancelMovement")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -622,25 +629,28 @@ export default function MovementsPage() {
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Elimina Movimento</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.movements.dialogs.delete.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Eliminare definitivamente il movimento{" "}
|
||||
<strong>{menuMovement?.documentNumber}</strong>?
|
||||
<Trans
|
||||
i18nKey="warehouse.movements.dialogs.delete.content"
|
||||
values={{ doc: menuMovement?.documentNumber }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
||||
Questa azione non può essere annullata.
|
||||
{t("warehouse.movements.dialogs.delete.warning")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>{t("warehouse.movements.dialogs.delete.cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleDeleteMovement}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
|
||||
{deleteMutation.isPending ? t("warehouse.movements.dialogs.delete.deleting") : t("warehouse.movements.dialogs.delete.delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Check as ConfirmIcon,
|
||||
Warning as WarningIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@@ -57,6 +58,7 @@ interface MovementLine {
|
||||
}
|
||||
|
||||
export default function OutboundMovementPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [movementDate, setMovementDate] = useState<Dayjs | null>(dayjs());
|
||||
const [warehouseId, setWarehouseId] = useState<number | "">("");
|
||||
@@ -138,14 +140,14 @@ export default function OutboundMovementPage() {
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!warehouseId) {
|
||||
newErrors.warehouseId = "Seleziona un magazzino";
|
||||
newErrors.warehouseId = t("warehouse.outbound.validation.warehouseRequired");
|
||||
}
|
||||
if (!movementDate) {
|
||||
newErrors.movementDate = "Inserisci la data";
|
||||
newErrors.movementDate = t("warehouse.outbound.validation.dateRequired");
|
||||
}
|
||||
const validLines = lines.filter((l) => l.article && l.quantity > 0);
|
||||
if (validLines.length === 0) {
|
||||
newErrors.lines = "Inserisci almeno una riga con articolo e quantità";
|
||||
newErrors.lines = t("warehouse.outbound.validation.linesRequired");
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -192,36 +194,35 @@ export default function OutboundMovementPage() {
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Nuovo Scarico
|
||||
{t("warehouse.outbound.title")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Movimento di uscita merce da magazzino
|
||||
{t("warehouse.outbound.subtitle")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{(createMutation.error || confirmMutation.error) && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
Errore:{" "}
|
||||
{((createMutation.error || confirmMutation.error) as Error).message}
|
||||
{t("warehouse.outbound.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{hasStockIssues && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||
Attenzione: alcune righe superano la disponibilità in magazzino
|
||||
{t("warehouse.outbound.warnings.stockIssues")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Form Header */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Dati Movimento
|
||||
{t("warehouse.outbound.sections.movementData")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<DatePicker
|
||||
label="Data Movimento"
|
||||
label={t("warehouse.outbound.fields.date")}
|
||||
value={movementDate}
|
||||
onChange={setMovementDate}
|
||||
slotProps={{
|
||||
@@ -236,10 +237,10 @@ export default function OutboundMovementPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth required error={!!errors.warehouseId}>
|
||||
<InputLabel>Magazzino</InputLabel>
|
||||
<InputLabel>{t("warehouse.outbound.fields.warehouse")}</InputLabel>
|
||||
<Select
|
||||
value={warehouseId}
|
||||
label="Magazzino"
|
||||
label={t("warehouse.outbound.fields.warehouse")}
|
||||
onChange={(e) => setWarehouseId(e.target.value as number)}
|
||||
>
|
||||
{warehouses?.map((w) => (
|
||||
@@ -261,25 +262,25 @@ export default function OutboundMovementPage() {
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Numero Documento"
|
||||
label={t("warehouse.outbound.fields.documentNumber")}
|
||||
value={documentNumber}
|
||||
onChange={(e) => setDocumentNumber(e.target.value)}
|
||||
placeholder="DDT, Bolla, etc."
|
||||
placeholder={t("warehouse.outbound.placeholders.documentNumber")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Riferimento Esterno"
|
||||
label={t("warehouse.outbound.fields.externalReference")}
|
||||
value={externalReference}
|
||||
onChange={(e) => setExternalReference(e.target.value)}
|
||||
placeholder="Ordine, Cliente, etc."
|
||||
placeholder={t("warehouse.outbound.placeholders.externalReference")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Note"
|
||||
label={t("warehouse.outbound.fields.notes")}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
multiline
|
||||
@@ -299,9 +300,9 @@ export default function OutboundMovementPage() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Righe Movimento</Typography>
|
||||
<Typography variant="h6">{t("warehouse.outbound.sections.lines")}</Typography>
|
||||
<Button startIcon={<AddIcon />} onClick={handleAddLine}>
|
||||
Aggiungi Riga
|
||||
{t("warehouse.outbound.actions.addLine")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -315,14 +316,14 @@ export default function OutboundMovementPage() {
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: "40%" }}>Articolo</TableCell>
|
||||
<TableCell sx={{ width: "40%" }}>{t("warehouse.outbound.fields.article")}</TableCell>
|
||||
<TableCell sx={{ width: "15%" }} align="right">
|
||||
Disponibile
|
||||
{t("warehouse.outbound.fields.available")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "15%" }} align="right">
|
||||
Quantità
|
||||
{t("warehouse.outbound.fields.quantity")}
|
||||
</TableCell>
|
||||
<TableCell sx={{ width: "20%" }}>Note</TableCell>
|
||||
<TableCell sx={{ width: "20%" }}>{t("warehouse.outbound.fields.notes")}</TableCell>
|
||||
<TableCell sx={{ width: 60 }}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -351,7 +352,7 @@ export default function OutboundMovementPage() {
|
||||
<TextField
|
||||
{...params}
|
||||
size="small"
|
||||
placeholder="Seleziona articolo"
|
||||
placeholder={t("warehouse.outbound.placeholders.selectArticle")}
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
@@ -401,7 +402,7 @@ export default function OutboundMovementPage() {
|
||||
fullWidth
|
||||
/>
|
||||
{isOverStock && (
|
||||
<Tooltip title="Quantità superiore alla disponibilità">
|
||||
<Tooltip title={t("warehouse.outbound.warnings.overStock")}>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
sx={{ fontSize: 16, ml: 1 }}
|
||||
@@ -413,7 +414,7 @@ export default function OutboundMovementPage() {
|
||||
<TableCell>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Note"
|
||||
placeholder={t("warehouse.outbound.placeholders.notes")}
|
||||
value={line.notes || ""}
|
||||
onChange={(e) =>
|
||||
handleLineChange(line.id, "notes", e.target.value)
|
||||
@@ -443,7 +444,7 @@ export default function OutboundMovementPage() {
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Totale Quantità
|
||||
{t("warehouse.outbound.totals.quantity")}
|
||||
</Typography>
|
||||
<Typography variant="h6">{totalQuantity.toFixed(2)}</Typography>
|
||||
</Box>
|
||||
@@ -452,7 +453,7 @@ export default function OutboundMovementPage() {
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
|
||||
<Button onClick={() => navigate(-1)}>Annulla</Button>
|
||||
<Button onClick={() => navigate(-1)}>{t("warehouse.outbound.actions.cancel")}</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={
|
||||
@@ -461,7 +462,7 @@ export default function OutboundMovementPage() {
|
||||
onClick={() => handleSubmit(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Salva Bozza
|
||||
{t("warehouse.outbound.actions.saveDraft")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -472,7 +473,7 @@ export default function OutboundMovementPage() {
|
||||
disabled={isPending || hasStockIssues}
|
||||
color="success"
|
||||
>
|
||||
Salva e Conferma
|
||||
{t("warehouse.outbound.actions.saveAndConfirm")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Warning as WarningIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { useStockLevels, useWarehouses, useCategoryTree } from "../hooks";
|
||||
import { useStockCalculations } from "../hooks/useStockCalculations";
|
||||
@@ -32,6 +33,7 @@ import { useWarehouseNavigation } from "../hooks/useWarehouseNavigation";
|
||||
import { StockLevelDto, formatCurrency, formatQuantity } from "../types";
|
||||
|
||||
export default function StockLevelsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [warehouseId, setWarehouseId] = useState<number | "">("");
|
||||
const [categoryId, setCategoryId] = useState<number | "">("");
|
||||
@@ -82,7 +84,7 @@ export default function StockLevelsPage() {
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "articleCode",
|
||||
headerName: "Codice",
|
||||
headerName: t("warehouse.stockLevels.columns.code"),
|
||||
width: 120,
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) => (
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
@@ -92,23 +94,23 @@ export default function StockLevelsPage() {
|
||||
},
|
||||
{
|
||||
field: "articleDescription",
|
||||
headerName: "Articolo",
|
||||
headerName: t("warehouse.stockLevels.columns.article"),
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: "warehouseName",
|
||||
headerName: "Magazzino",
|
||||
headerName: t("warehouse.stockLevels.columns.warehouse"),
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: "categoryName",
|
||||
headerName: "Categoria",
|
||||
headerName: t("warehouse.stockLevels.columns.category"),
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
field: "quantity",
|
||||
headerName: "Giacenza",
|
||||
headerName: t("warehouse.stockLevels.columns.quantity"),
|
||||
width: 120,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) => {
|
||||
@@ -128,7 +130,7 @@ export default function StockLevelsPage() {
|
||||
},
|
||||
{
|
||||
field: "reservedQuantity",
|
||||
headerName: "Riservata",
|
||||
headerName: t("warehouse.stockLevels.columns.reserved"),
|
||||
width: 100,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) =>
|
||||
@@ -136,7 +138,7 @@ export default function StockLevelsPage() {
|
||||
},
|
||||
{
|
||||
field: "availableQuantity",
|
||||
headerName: "Disponibile",
|
||||
headerName: t("warehouse.stockLevels.columns.available"),
|
||||
width: 110,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) => {
|
||||
@@ -155,7 +157,7 @@ export default function StockLevelsPage() {
|
||||
},
|
||||
{
|
||||
field: "unitCost",
|
||||
headerName: "Costo Medio",
|
||||
headerName: t("warehouse.stockLevels.columns.averageCost"),
|
||||
width: 120,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) =>
|
||||
@@ -163,7 +165,7 @@ export default function StockLevelsPage() {
|
||||
},
|
||||
{
|
||||
field: "stockValue",
|
||||
headerName: "Valore",
|
||||
headerName: t("warehouse.stockLevels.columns.value"),
|
||||
width: 130,
|
||||
align: "right",
|
||||
renderCell: (params: GridRenderCellParams<StockLevelDto>) => (
|
||||
@@ -177,7 +179,7 @@ export default function StockLevelsPage() {
|
||||
if (error) {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Alert severity="error">Errore: {(error as Error).message}</Alert>
|
||||
<Alert severity="error">{t("warehouse.stockLevels.error", { error: (error as Error).message })}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -194,14 +196,14 @@ export default function StockLevelsPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Giacenze di Magazzino
|
||||
{t("warehouse.stockLevels.title")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<TrendingUpIcon />}
|
||||
onClick={nav.goToValuation}
|
||||
>
|
||||
Valorizzazione
|
||||
{t("warehouse.stockLevels.valuation")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -211,7 +213,7 @@ export default function StockLevelsPage() {
|
||||
<Card>
|
||||
<CardContent sx={{ py: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Articoli
|
||||
{t("warehouse.stockLevels.summary.articles")}
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
{summary.articleCount}
|
||||
@@ -223,7 +225,7 @@ export default function StockLevelsPage() {
|
||||
<Card>
|
||||
<CardContent sx={{ py: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Quantità Totale
|
||||
{t("warehouse.stockLevels.summary.totalQuantity")}
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
{formatQuantity(summary.totalQuantity)}
|
||||
@@ -235,7 +237,7 @@ export default function StockLevelsPage() {
|
||||
<Card>
|
||||
<CardContent sx={{ py: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Valore Totale
|
||||
{t("warehouse.stockLevels.summary.totalValue")}
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight="bold" color="success.main">
|
||||
{formatCurrency(summary.totalValue)}
|
||||
@@ -247,7 +249,7 @@ export default function StockLevelsPage() {
|
||||
<Card>
|
||||
<CardContent sx={{ py: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Sotto Scorta
|
||||
{t("warehouse.stockLevels.summary.lowStock")}
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight="bold" color="warning.main">
|
||||
{summary.lowStockCount + summary.outOfStockCount}
|
||||
@@ -264,7 +266,7 @@ export default function StockLevelsPage() {
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Cerca articolo..."
|
||||
placeholder={t("warehouse.stockLevels.filters.search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
InputProps={{
|
||||
@@ -285,14 +287,14 @@ export default function StockLevelsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Magazzino</InputLabel>
|
||||
<InputLabel>{t("warehouse.stockLevels.filters.warehouse")}</InputLabel>
|
||||
<Select
|
||||
value={warehouseId}
|
||||
label="Magazzino"
|
||||
label={t("warehouse.stockLevels.filters.warehouse")}
|
||||
onChange={(e) => setWarehouseId(e.target.value as number | "")}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutti</em>
|
||||
<em>{t("warehouse.stockLevels.filters.allWarehouses")}</em>
|
||||
</MenuItem>
|
||||
{warehouses?.map((w) => (
|
||||
<MenuItem key={w.id} value={w.id}>
|
||||
@@ -304,14 +306,14 @@ export default function StockLevelsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<InputLabel>{t("warehouse.stockLevels.filters.category")}</InputLabel>
|
||||
<Select
|
||||
value={categoryId}
|
||||
label="Categoria"
|
||||
label={t("warehouse.stockLevels.filters.category")}
|
||||
onChange={(e) => setCategoryId(e.target.value as number | "")}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>Tutte</em>
|
||||
<em>{t("warehouse.stockLevels.filters.allCategories")}</em>
|
||||
</MenuItem>
|
||||
{flatCategories.map((c) => (
|
||||
<MenuItem key={c.id} value={c.id}>
|
||||
@@ -329,7 +331,7 @@ export default function StockLevelsPage() {
|
||||
onChange={(e) => setLowStockOnly(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Solo sotto scorta"
|
||||
label={t("warehouse.stockLevels.filters.lowStockOnly")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
Check as ConfirmIcon,
|
||||
SwapHoriz as TransferIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@@ -56,6 +57,7 @@ interface MovementLine {
|
||||
}
|
||||
|
||||
export default function TransferMovementPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [movementDate, setMovementDate] = useState<Dayjs | null>(dayjs());
|
||||
const [sourceWarehouseId, setSourceWarehouseId] = useState<number | "">("");
|
||||
@@ -120,10 +122,10 @@ export default function TransferMovementPage() {
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!sourceWarehouseId) {
|
||||
newErrors.sourceWarehouseId = "Seleziona magazzino origine";
|
||||
newErrors.sourceWarehouseId = t("warehouse.transfer.validation.sourceRequired");
|
||||
}
|
||||
if (!destWarehouseId) {
|
||||
newErrors.destWarehouseId = "Seleziona magazzino destinazione";
|
||||
newErrors.destWarehouseId = t("warehouse.transfer.validation.destRequired");
|
||||
}
|
||||
if (
|
||||
sourceWarehouseId &&
|
||||
@@ -131,14 +133,14 @@ export default function TransferMovementPage() {
|
||||
sourceWarehouseId === destWarehouseId
|
||||
) {
|
||||
newErrors.destWarehouseId =
|
||||
"Origine e destinazione devono essere diversi";
|
||||
t("warehouse.transfer.validation.sameWarehouse");
|
||||
}
|
||||
if (!movementDate) {
|
||||
newErrors.movementDate = "Inserisci la data";
|
||||
newErrors.movementDate = t("warehouse.transfer.validation.dateRequired");
|
||||
}
|
||||
const validLines = lines.filter((l) => l.article && l.quantity > 0);
|
||||
if (validLines.length === 0) {
|
||||
newErrors.lines = "Inserisci almeno una riga";
|
||||
newErrors.lines = t("warehouse.transfer.validation.linesRequired");
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -188,10 +190,10 @@ export default function TransferMovementPage() {
|
||||
<TransferIcon color="primary" />
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Trasferimento tra Magazzini
|
||||
{t("warehouse.transfer.title")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Sposta merce da un magazzino all'altro
|
||||
{t("warehouse.transfer.subtitle")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -199,20 +201,19 @@ export default function TransferMovementPage() {
|
||||
|
||||
{(createMutation.error || confirmMutation.error) && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
Errore:{" "}
|
||||
{((createMutation.error || confirmMutation.error) as Error).message}
|
||||
{t("warehouse.transfer.errors.saveError", { error: ((createMutation.error || confirmMutation.error) as Error).message })}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Dati Trasferimento
|
||||
{t("warehouse.transfer.sections.transferData")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<DatePicker
|
||||
label="Data"
|
||||
label={t("warehouse.transfer.fields.date")}
|
||||
value={movementDate}
|
||||
onChange={setMovementDate}
|
||||
slotProps={{
|
||||
@@ -231,10 +232,10 @@ export default function TransferMovementPage() {
|
||||
required
|
||||
error={!!errors.sourceWarehouseId}
|
||||
>
|
||||
<InputLabel>Magazzino Origine</InputLabel>
|
||||
<InputLabel>{t("warehouse.transfer.fields.sourceWarehouse")}</InputLabel>
|
||||
<Select
|
||||
value={sourceWarehouseId}
|
||||
label="Magazzino Origine"
|
||||
label={t("warehouse.transfer.fields.sourceWarehouse")}
|
||||
onChange={(e) =>
|
||||
setSourceWarehouseId(e.target.value as number)
|
||||
}
|
||||
@@ -251,10 +252,10 @@ export default function TransferMovementPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<FormControl fullWidth required error={!!errors.destWarehouseId}>
|
||||
<InputLabel>Magazzino Destinazione</InputLabel>
|
||||
<InputLabel>{t("warehouse.transfer.fields.destWarehouse")}</InputLabel>
|
||||
<Select
|
||||
value={destWarehouseId}
|
||||
label="Magazzino Destinazione"
|
||||
label={t("warehouse.transfer.fields.destWarehouse")}
|
||||
onChange={(e) => setDestWarehouseId(e.target.value as number)}
|
||||
>
|
||||
{warehouses
|
||||
@@ -275,7 +276,7 @@ export default function TransferMovementPage() {
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Documento"
|
||||
label={t("warehouse.transfer.fields.document")}
|
||||
value={documentNumber}
|
||||
onChange={(e) => setDocumentNumber(e.target.value)}
|
||||
/>
|
||||
@@ -283,7 +284,7 @@ export default function TransferMovementPage() {
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Riferimento Esterno"
|
||||
label={t("warehouse.transfer.fields.externalReference")}
|
||||
value={externalReference}
|
||||
onChange={(e) => setExternalReference(e.target.value)}
|
||||
/>
|
||||
@@ -291,7 +292,7 @@ export default function TransferMovementPage() {
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Note"
|
||||
label={t("warehouse.transfer.fields.notes")}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
/>
|
||||
@@ -302,9 +303,9 @@ export default function TransferMovementPage() {
|
||||
{/* Lines */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}>
|
||||
<Typography variant="h6">Articoli da Trasferire</Typography>
|
||||
<Typography variant="h6">{t("warehouse.transfer.sections.lines")}</Typography>
|
||||
<Button startIcon={<AddIcon />} onClick={handleAddLine}>
|
||||
Aggiungi
|
||||
{t("warehouse.transfer.actions.add")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -318,10 +319,10 @@ export default function TransferMovementPage() {
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Articolo</TableCell>
|
||||
<TableCell align="right">Disponibile</TableCell>
|
||||
<TableCell align="right">Quantità</TableCell>
|
||||
<TableCell>Note</TableCell>
|
||||
<TableCell>{t("warehouse.transfer.fields.article")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.transfer.fields.available")}</TableCell>
|
||||
<TableCell align="right">{t("warehouse.transfer.fields.quantity")}</TableCell>
|
||||
<TableCell>{t("warehouse.transfer.fields.notes")}</TableCell>
|
||||
<TableCell width={60}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -340,7 +341,7 @@ export default function TransferMovementPage() {
|
||||
<TextField
|
||||
{...params}
|
||||
size="small"
|
||||
placeholder="Articolo"
|
||||
placeholder={t("warehouse.transfer.placeholders.article")}
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(o, v) => o.id === v.id}
|
||||
@@ -389,7 +390,7 @@ export default function TransferMovementPage() {
|
||||
onChange={(e) =>
|
||||
handleLineChange(line.id, "notes", e.target.value)
|
||||
}
|
||||
placeholder="Note"
|
||||
placeholder={t("warehouse.transfer.placeholders.notes")}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -411,14 +412,14 @@ export default function TransferMovementPage() {
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Typography variant="h6">
|
||||
Totale: {formatQuantity(totalQuantity)}
|
||||
{t("warehouse.transfer.totals.total", { value: formatQuantity(totalQuantity) })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 2 }}>
|
||||
<Button onClick={() => navigate(-1)}>Annulla</Button>
|
||||
<Button onClick={() => navigate(-1)}>{t("warehouse.transfer.actions.cancel")}</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={
|
||||
@@ -427,7 +428,7 @@ export default function TransferMovementPage() {
|
||||
onClick={() => handleSubmit(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Salva Bozza
|
||||
{t("warehouse.transfer.actions.saveDraft")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -438,7 +439,7 @@ export default function TransferMovementPage() {
|
||||
onClick={() => handleSubmit(true)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Salva e Conferma
|
||||
{t("warehouse.transfer.actions.saveAndConfirm")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Add as AddIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useArticles,
|
||||
useWarehouses,
|
||||
@@ -107,6 +108,7 @@ function StatCard({
|
||||
|
||||
export default function WarehouseDashboard() {
|
||||
const nav = useWarehouseNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: articles, isLoading: loadingArticles } = useArticles({
|
||||
isActive: true,
|
||||
@@ -189,14 +191,14 @@ export default function WarehouseDashboard() {
|
||||
startIcon={<AddIcon />}
|
||||
onClick={nav.goToNewInbound}
|
||||
>
|
||||
Nuovo Carico
|
||||
{t("warehouse.dashboard.newInbound")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AssessmentIcon />}
|
||||
onClick={nav.goToStockLevels}
|
||||
>
|
||||
Giacenze
|
||||
{t("warehouse.dashboard.stockLevels")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -204,7 +206,7 @@ export default function WarehouseDashboard() {
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Articoli Attivi"
|
||||
title={t("warehouse.dashboard.activeArticles")}
|
||||
value={totalArticles}
|
||||
icon={<InventoryIcon />}
|
||||
loading={loadingArticles}
|
||||
@@ -212,7 +214,7 @@ export default function WarehouseDashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Magazzini"
|
||||
title={t("warehouse.dashboard.warehouses")}
|
||||
value={totalWarehouses}
|
||||
icon={<WarehouseIcon />}
|
||||
loading={loadingWarehouses}
|
||||
@@ -220,7 +222,7 @@ export default function WarehouseDashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Valore Totale"
|
||||
title={t("warehouse.dashboard.totalValue")}
|
||||
value={formatCurrency(stockStats.totalValue)}
|
||||
icon={<TrendingUpIcon />}
|
||||
color="success.main"
|
||||
@@ -229,9 +231,9 @@ export default function WarehouseDashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Sotto Scorta"
|
||||
title={t("warehouse.dashboard.lowStock")}
|
||||
value={stockStats.lowStock + stockStats.outOfStock}
|
||||
subtitle={`${stockStats.outOfStock} esauriti`}
|
||||
subtitle={`${stockStats.outOfStock} ${t("warehouse.dashboard.outOfStock")}`}
|
||||
icon={<WarningIcon />}
|
||||
color="warning.main"
|
||||
loading={loadingStock}
|
||||
@@ -252,13 +254,13 @@ export default function WarehouseDashboard() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Ultimi Movimenti</Typography>
|
||||
<Typography variant="h6">{t("warehouse.dashboard.recentMovements")}</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowForwardIcon />}
|
||||
onClick={nav.goToMovements}
|
||||
>
|
||||
Vedi tutti
|
||||
{t("warehouse.dashboard.viewAll")}
|
||||
</Button>
|
||||
</Box>
|
||||
{loadingMovements ? (
|
||||
@@ -269,7 +271,7 @@ export default function WarehouseDashboard() {
|
||||
</Box>
|
||||
) : lastMovements.length === 0 ? (
|
||||
<Typography color="text.secondary" textAlign="center" py={4}>
|
||||
Nessun movimento recente
|
||||
{t("warehouse.dashboard.noRecentMovements")}
|
||||
</Typography>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
@@ -317,7 +319,7 @@ export default function WarehouseDashboard() {
|
||||
secondary={`${movement.sourceWarehouseName || movement.destinationWarehouseName || "-"} - ${formatDate(movement.movementDate)}`}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{formatQuantity(movement.lineCount)} righe
|
||||
{formatQuantity(movement.lineCount)} {t("warehouse.dashboard.lines")}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
{index < lastMovements.length - 1 && <Divider />}
|
||||
@@ -339,12 +341,11 @@ export default function WarehouseDashboard() {
|
||||
icon={<ScheduleIcon />}
|
||||
action={
|
||||
<Button size="small" onClick={nav.goToMovements}>
|
||||
Gestisci
|
||||
{t("warehouse.dashboard.manage")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<strong>{pendingMovements.length}</strong> movimenti in bozza
|
||||
da confermare
|
||||
<strong>{pendingMovements.length}</strong> {t("warehouse.dashboard.draftMovements")}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -357,12 +358,11 @@ export default function WarehouseDashboard() {
|
||||
icon={<ScheduleIcon />}
|
||||
action={
|
||||
<Button size="small" onClick={nav.goToBatches}>
|
||||
Visualizza
|
||||
{t("warehouse.dashboard.view")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<strong>{expiringBatches.length}</strong> lotti in scadenza
|
||||
nei prossimi 30 giorni
|
||||
<strong>{expiringBatches.length}</strong> {t("warehouse.dashboard.expiringBatches")}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -378,18 +378,18 @@ export default function WarehouseDashboard() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Articoli Sotto Scorta</Typography>
|
||||
<Typography variant="h6">{t("warehouse.dashboard.lowStockArticles")}</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowForwardIcon />}
|
||||
onClick={nav.goToArticles}
|
||||
>
|
||||
Vedi tutti
|
||||
{t("warehouse.dashboard.viewAll")}
|
||||
</Button>
|
||||
</Box>
|
||||
{lowStockArticles.length === 0 ? (
|
||||
<Typography color="text.secondary" textAlign="center" py={2}>
|
||||
Nessun articolo sotto scorta
|
||||
{t("warehouse.dashboard.noLowStockArticles")}
|
||||
</Typography>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
@@ -429,7 +429,7 @@ export default function WarehouseDashboard() {
|
||||
<Grid size={12}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Azioni Rapide
|
||||
{t("warehouse.dashboard.quickActions")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6, sm: 4, md: 2 }}>
|
||||
@@ -443,7 +443,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<TrendingUpIcon color="success" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Carico</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.inbound")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -458,7 +458,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<TrendingDownIcon color="error" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Scarico</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.outbound")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -473,7 +473,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<WarehouseIcon color="primary" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Trasferimento</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.transfer")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -488,7 +488,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<InventoryIcon color="info" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Nuovo Articolo</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.newArticle")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -503,7 +503,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<AssessmentIcon color="secondary" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Inventario</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.inventory")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -518,7 +518,7 @@ export default function WarehouseDashboard() {
|
||||
>
|
||||
<CardContent>
|
||||
<TrendingUpIcon color="warning" sx={{ fontSize: 40 }} />
|
||||
<Typography variant="body2">Valorizzazione</Typography>
|
||||
<Typography variant="body2">{t("warehouse.dashboard.valuation")}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
StarBorder as StarBorderIcon,
|
||||
Warehouse as WarehouseIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import {
|
||||
useWarehouses,
|
||||
useCreateWarehouse,
|
||||
@@ -58,6 +59,7 @@ const initialFormData = {
|
||||
};
|
||||
|
||||
export default function WarehouseLocationsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [editingWarehouse, setEditingWarehouse] =
|
||||
@@ -113,7 +115,7 @@ export default function WarehouseLocationsPage() {
|
||||
const newErrors: Record<string, string> = {};
|
||||
// Il codice è generato automaticamente, non richiede validazione in creazione
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = "Il nome è obbligatorio";
|
||||
newErrors.name = t("warehouse.locations.dialog.validation.nameRequired");
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -183,7 +185,7 @@ export default function WarehouseLocationsPage() {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Alert severity="error">
|
||||
Errore nel caricamento dei magazzini: {(error as Error).message}
|
||||
{t("warehouse.locations.loadingError", { error: (error as Error).message })}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
@@ -201,14 +203,14 @@ export default function WarehouseLocationsPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Gestione Magazzini
|
||||
{t("warehouse.locations.title")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenDialog()}
|
||||
>
|
||||
Nuovo Magazzino
|
||||
{t("warehouse.locations.newWarehouse")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -231,7 +233,7 @@ export default function WarehouseLocationsPage() {
|
||||
<Paper sx={{ p: 4, textAlign: "center" }}>
|
||||
<WarehouseIcon sx={{ fontSize: 48, color: "grey.400", mb: 2 }} />
|
||||
<Typography color="text.secondary">
|
||||
Nessun magazzino configurato
|
||||
{t("warehouse.locations.emptyState.title")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -239,7 +241,7 @@ export default function WarehouseLocationsPage() {
|
||||
onClick={() => handleOpenDialog()}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Aggiungi il primo magazzino
|
||||
{t("warehouse.locations.emptyState.action")}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
@@ -256,7 +258,7 @@ export default function WarehouseLocationsPage() {
|
||||
}}
|
||||
>
|
||||
{warehouse.isDefault && (
|
||||
<Tooltip title="Magazzino Predefinito">
|
||||
<Tooltip title={t("warehouse.locations.card.default")}>
|
||||
<StarIcon
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@@ -297,12 +299,12 @@ export default function WarehouseLocationsPage() {
|
||||
sx={{ mt: 2, display: "flex", gap: 1, flexWrap: "wrap" }}
|
||||
>
|
||||
<Chip
|
||||
label={warehouseTypeLabels[warehouse.type]}
|
||||
label={t(`warehouse.warehouseType.${WarehouseType[warehouse.type]}`)}
|
||||
size="small"
|
||||
color={getTypeColor(warehouse.type)}
|
||||
/>
|
||||
{!warehouse.isActive && (
|
||||
<Chip label="Inattivo" size="small" color="default" />
|
||||
<Chip label={t("warehouse.locations.card.inactive")} size="small" color="default" />
|
||||
)}
|
||||
</Box>
|
||||
{warehouse.address && (
|
||||
@@ -317,7 +319,7 @@ export default function WarehouseLocationsPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Tooltip title="Imposta come predefinito">
|
||||
<Tooltip title={t("warehouse.locations.card.setDefault")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleSetDefault(warehouse)}
|
||||
@@ -332,7 +334,7 @@ export default function WarehouseLocationsPage() {
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Modifica">
|
||||
<Tooltip title={t("warehouse.locations.card.edit")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleOpenDialog(warehouse)}
|
||||
@@ -340,7 +342,7 @@ export default function WarehouseLocationsPage() {
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Elimina">
|
||||
<Tooltip title={t("warehouse.locations.card.delete")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleDeleteClick(warehouse)}
|
||||
@@ -364,22 +366,22 @@ export default function WarehouseLocationsPage() {
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{editingWarehouse ? "Modifica Magazzino" : "Nuovo Magazzino"}
|
||||
{editingWarehouse ? t("warehouse.locations.dialog.editTitle") : t("warehouse.locations.dialog.createTitle")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice"
|
||||
label={t("warehouse.locations.dialog.fields.code")}
|
||||
value={
|
||||
editingWarehouse ? formData.code : "(Generato al salvataggio)"
|
||||
editingWarehouse ? formData.code : t("warehouse.locations.dialog.helpers.generatedOnSave")
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingWarehouse
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
? t("warehouse.locations.dialog.helpers.generatedAutomatically")
|
||||
: t("warehouse.locations.dialog.helpers.willBeAssigned")
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -387,11 +389,11 @@ export default function WarehouseLocationsPage() {
|
||||
sx={
|
||||
!editingWarehouse
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -399,18 +401,18 @@ export default function WarehouseLocationsPage() {
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice Alternativo"
|
||||
label={t("warehouse.locations.dialog.fields.alternativeCode")}
|
||||
value={formData.alternativeCode}
|
||||
onChange={(e) =>
|
||||
handleChange("alternativeCode", e.target.value)
|
||||
}
|
||||
helperText="Opzionale"
|
||||
helperText={t("warehouse.locations.dialog.helpers.optional")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Nome"
|
||||
label={t("warehouse.locations.dialog.fields.name")}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange("name", e.target.value)}
|
||||
error={!!errors.name}
|
||||
@@ -421,7 +423,7 @@ export default function WarehouseLocationsPage() {
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Descrizione"
|
||||
label={t("warehouse.locations.dialog.fields.description")}
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange("description", e.target.value)}
|
||||
multiline
|
||||
@@ -430,15 +432,15 @@ export default function WarehouseLocationsPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo</InputLabel>
|
||||
<InputLabel>{t("warehouse.locations.dialog.fields.type")}</InputLabel>
|
||||
<Select
|
||||
value={formData.type}
|
||||
label="Tipo"
|
||||
label={t("warehouse.locations.dialog.fields.type")}
|
||||
onChange={(e) => handleChange("type", e.target.value)}
|
||||
>
|
||||
{Object.entries(warehouseTypeLabels).map(([value, label]) => (
|
||||
{Object.entries(warehouseTypeLabels).map(([value]) => (
|
||||
<MenuItem key={value} value={parseInt(value, 10)}>
|
||||
{label}
|
||||
{t(`warehouse.warehouseType.${WarehouseType[parseInt(value, 10)]}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -447,7 +449,7 @@ export default function WarehouseLocationsPage() {
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Indirizzo"
|
||||
label={t("warehouse.locations.dialog.fields.address")}
|
||||
value={formData.address}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
/>
|
||||
@@ -462,7 +464,7 @@ export default function WarehouseLocationsPage() {
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Magazzino Predefinito"
|
||||
label={t("warehouse.locations.dialog.fields.isDefault")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
@@ -473,21 +475,21 @@ export default function WarehouseLocationsPage() {
|
||||
onChange={(e) => handleChange("isActive", e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Attivo"
|
||||
label={t("warehouse.locations.dialog.fields.isActive")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t("warehouse.locations.dialog.actions.cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
disabled={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending || updateMutation.isPending
|
||||
? "Salvataggio..."
|
||||
: "Salva"}
|
||||
? t("warehouse.locations.dialog.actions.saving")
|
||||
: t("warehouse.locations.dialog.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -497,28 +499,28 @@ export default function WarehouseLocationsPage() {
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Conferma Eliminazione</DialogTitle>
|
||||
<DialogTitle>{t("warehouse.locations.deleteDialog.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler eliminare il magazzino{" "}
|
||||
<strong>
|
||||
{warehouseToDelete?.code} - {warehouseToDelete?.name}
|
||||
</strong>
|
||||
?
|
||||
<Trans
|
||||
i18nKey="warehouse.locations.deleteDialog.content"
|
||||
values={{ code: warehouseToDelete?.code, name: warehouseToDelete?.name }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Questa azione non può essere annullata.
|
||||
{t("warehouse.locations.deleteDialog.warning")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>{t("warehouse.locations.deleteDialog.cancel")}</Button>
|
||||
<Button
|
||||
onClick={handleDeleteConfirm}
|
||||
color="error"
|
||||
variant="contained"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
|
||||
{deleteMutation.isPending ? t("warehouse.locations.deleteDialog.deleting") : t("warehouse.locations.deleteDialog.delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -23,11 +23,13 @@ import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { articoliService, lookupService } from "../services/lookupService";
|
||||
import { Articolo } from "../types";
|
||||
|
||||
export default function ArticoliPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Articolo>>({ attivo: true });
|
||||
@@ -94,34 +96,34 @@ export default function ArticoliPage() {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: "codice", headerName: "Codice", width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
|
||||
{ field: "descrizione", headerName: "Descrizione", flex: 1, minWidth: 200 },
|
||||
{ field: "codice", headerName: t("articles.code"), width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: t("articles.altCode"), width: 100 },
|
||||
{ field: "descrizione", headerName: t("articles.description"), flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: "tipoMateriale",
|
||||
headerName: "Tipo",
|
||||
headerName: t("articles.type"),
|
||||
width: 130,
|
||||
valueGetter: (value: any) => value?.descrizione || "",
|
||||
},
|
||||
{
|
||||
field: "categoria",
|
||||
headerName: "Categoria",
|
||||
headerName: t("articles.category"),
|
||||
width: 120,
|
||||
valueGetter: (value: any) => value?.descrizione || "",
|
||||
},
|
||||
{
|
||||
field: "qtaDisponibile",
|
||||
headerName: "Disponibile",
|
||||
headerName: t("articles.available"),
|
||||
width: 100,
|
||||
type: "number",
|
||||
},
|
||||
{ field: "qtaStdA", headerName: "Qta A", width: 80, type: "number" },
|
||||
{ field: "qtaStdB", headerName: "Qta B", width: 80, type: "number" },
|
||||
{ field: "qtaStdS", headerName: "Qta S", width: 80, type: "number" },
|
||||
{ field: "unitaMisura", headerName: "UM", width: 60 },
|
||||
{ field: "qtaStdA", headerName: t("articles.qtyA"), width: 80, type: "number" },
|
||||
{ field: "qtaStdB", headerName: t("articles.qtyB"), width: 80, type: "number" },
|
||||
{ field: "qtaStdS", headerName: t("articles.qtyS"), width: 80, type: "number" },
|
||||
{ field: "unitaMisura", headerName: t("articles.uom"), width: 60 },
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Azioni",
|
||||
headerName: t("common.actions"),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -133,7 +135,7 @@ export default function ArticoliPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm("Eliminare questo articolo?")) {
|
||||
if (confirm(t("articles.deleteConfirm"))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -155,13 +157,13 @@ export default function ArticoliPage() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Articoli</Typography>
|
||||
<Typography variant="h4">{t("articles.title")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpenDialog(true)}
|
||||
>
|
||||
Nuovo Articolo
|
||||
{t("articles.newArticle")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -185,24 +187,24 @@ export default function ArticoliPage() {
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{editingId ? "Modifica Articolo" : "Nuovo Articolo"}
|
||||
{editingId ? t("articles.editArticle") : t("articles.newArticle")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice"
|
||||
label={t("articles.code")}
|
||||
fullWidth
|
||||
value={
|
||||
editingId
|
||||
? formData.codice || ""
|
||||
: "(Generato al salvataggio)"
|
||||
: t("articles.generatedOnSave")
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingId
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
? t("articles.autoGenerated")
|
||||
: t("articles.willBeAssigned")
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -210,18 +212,18 @@ export default function ArticoliPage() {
|
||||
sx={
|
||||
!editingId
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice Alternativo"
|
||||
label={t("articles.altCode")}
|
||||
fullWidth
|
||||
value={formData.codiceAlternativo || ""}
|
||||
onChange={(e) =>
|
||||
@@ -230,12 +232,12 @@ export default function ArticoliPage() {
|
||||
codiceAlternativo: e.target.value,
|
||||
})
|
||||
}
|
||||
helperText="Opzionale"
|
||||
helperText={t("common.optional")}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
label={t("articles.description")}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.descrizione || ""}
|
||||
@@ -246,10 +248,10 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo Materiale</InputLabel>
|
||||
<InputLabel>{t("articles.materialType")}</InputLabel>
|
||||
<Select
|
||||
value={formData.tipoMaterialeId || ""}
|
||||
label="Tipo Materiale"
|
||||
label={t("articles.materialType")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -267,10 +269,10 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<InputLabel>{t("articles.category")}</InputLabel>
|
||||
<Select
|
||||
value={formData.categoriaId || ""}
|
||||
label="Categoria"
|
||||
label={t("articles.category")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -288,7 +290,7 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Quantità Disponibile"
|
||||
label={t("articles.qtyAvailable")}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaDisponibile || ""}
|
||||
@@ -302,7 +304,7 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Unità Misura"
|
||||
label={t("articles.unitOfMeasure")}
|
||||
fullWidth
|
||||
value={formData.unitaMisura || ""}
|
||||
onChange={(e) =>
|
||||
@@ -313,7 +315,7 @@ export default function ArticoliPage() {
|
||||
<Grid size={{ xs: 12, md: 4 }}></Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Qta Std Adulti (A)"
|
||||
label={t("articles.qtyStdAdults")}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdA || ""}
|
||||
@@ -327,7 +329,7 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Qta Std Buffet (B)"
|
||||
label={t("articles.qtyStdBuffet")}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdB || ""}
|
||||
@@ -341,7 +343,7 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Qta Std Seduti (S)"
|
||||
label={t("articles.qtyStdSeated")}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdS || ""}
|
||||
@@ -355,7 +357,7 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("common.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
@@ -368,9 +370,9 @@ export default function ArticoliPage() {
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t("common.cancel")}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? "Salva" : "Crea"}
|
||||
{editingId ? t("common.save") : t("common.create")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
ContentCopy as CopyIcon,
|
||||
} from "@mui/icons-material";
|
||||
import * as Icons from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { autoCodeService } from "../services/autoCodeService";
|
||||
import type {
|
||||
AutoCodeDto,
|
||||
@@ -57,6 +58,7 @@ import { groupByModule, moduleNames, moduleIcons } from "../types/autoCode";
|
||||
|
||||
export default function AutoCodesAdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [editingConfig, setEditingConfig] = useState<AutoCodeDto | null>(null);
|
||||
const [confirmReset, setConfirmReset] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
@@ -142,10 +144,10 @@ export default function AutoCodesAdminPage() {
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Codici Automatici
|
||||
{t("autoCodes.title")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Configura i pattern per la generazione automatica dei codici
|
||||
{t("autoCodes.subtitle")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
@@ -154,14 +156,14 @@ export default function AutoCodesAdminPage() {
|
||||
startIcon={<HelpIcon />}
|
||||
onClick={() => setShowHelp(true)}
|
||||
>
|
||||
Guida Pattern
|
||||
{t("autoCodes.helpPattern")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
Aggiorna
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -194,14 +196,14 @@ export default function AutoCodesAdminPage() {
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Entita</TableCell>
|
||||
<TableCell>Prefisso</TableCell>
|
||||
<TableCell>Pattern</TableCell>
|
||||
<TableCell>Esempio</TableCell>
|
||||
<TableCell>Sequenza</TableCell>
|
||||
<TableCell>Reset</TableCell>
|
||||
<TableCell align="center">Stato</TableCell>
|
||||
<TableCell align="right">Azioni</TableCell>
|
||||
<TableCell>{t("autoCodes.entity")}</TableCell>
|
||||
<TableCell>{t("autoCodes.prefix")}</TableCell>
|
||||
<TableCell>{t("autoCodes.pattern")}</TableCell>
|
||||
<TableCell>{t("autoCodes.example")}</TableCell>
|
||||
<TableCell>{t("autoCodes.sequence")}</TableCell>
|
||||
<TableCell>{t("autoCodes.reset")}</TableCell>
|
||||
<TableCell align="center">{t("autoCodes.status")}</TableCell>
|
||||
<TableCell align="right">{t("common.actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -250,7 +252,7 @@ export default function AutoCodesAdminPage() {
|
||||
>
|
||||
{config.exampleCode}
|
||||
</Typography>
|
||||
<Tooltip title="Anteprima prossimo codice">
|
||||
<Tooltip title={t("autoCodes.previewTooltip")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
@@ -270,22 +272,22 @@ export default function AutoCodesAdminPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{config.resetSequenceMonthly ? (
|
||||
<Chip label="Mensile" size="small" color="info" />
|
||||
<Chip label={t("autoCodes.monthly")} size="small" color="info" />
|
||||
) : config.resetSequenceYearly ? (
|
||||
<Chip label="Annuale" size="small" color="warning" />
|
||||
<Chip label={t("autoCodes.yearly")} size="small" color="warning" />
|
||||
) : (
|
||||
<Chip label="Mai" size="small" variant="outlined" />
|
||||
<Chip label={t("autoCodes.never")} size="small" variant="outlined" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={config.isEnabled ? "Attivo" : "Disattivo"}
|
||||
label={config.isEnabled ? t("modules.admin.active") : t("modules.admin.inactive")}
|
||||
size="small"
|
||||
color={config.isEnabled ? "success" : "default"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Modifica">
|
||||
<Tooltip title={t("common.edit")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingConfig(config)}
|
||||
@@ -293,7 +295,7 @@ export default function AutoCodesAdminPage() {
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset sequenza">
|
||||
<Tooltip title={t("autoCodes.resetTooltip")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setConfirmReset(config.entityCode)}
|
||||
@@ -324,6 +326,7 @@ export default function AutoCodesAdminPage() {
|
||||
}}
|
||||
isSaving={updateMutation.isPending}
|
||||
error={updateMutation.error as Error | null}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Dialog conferma reset */}
|
||||
@@ -333,22 +336,21 @@ export default function AutoCodesAdminPage() {
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Conferma Reset Sequenza</DialogTitle>
|
||||
<DialogTitle>{t("autoCodes.resetConfirmTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler resettare la sequenza per{" "}
|
||||
{t("autoCodes.resetConfirmText")}{" "}
|
||||
<strong>
|
||||
{configs.find((c) => c.entityCode === confirmReset)?.entityName}
|
||||
</strong>
|
||||
?
|
||||
</Typography>
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
La sequenza verra riportata a 0. Il prossimo codice generato partira
|
||||
da 1.
|
||||
{t("autoCodes.resetWarning")}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmReset(null)}>Annulla</Button>
|
||||
<Button onClick={() => setConfirmReset(null)}>{t("common.cancel")}</Button>
|
||||
<Button
|
||||
color="warning"
|
||||
variant="contained"
|
||||
@@ -362,7 +364,7 @@ export default function AutoCodesAdminPage() {
|
||||
)
|
||||
}
|
||||
>
|
||||
Reset
|
||||
{t("autoCodes.reset")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -374,7 +376,7 @@ export default function AutoCodesAdminPage() {
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Anteprima Prossimo Codice</DialogTitle>
|
||||
<DialogTitle>{t("autoCodes.previewTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -410,13 +412,11 @@ export default function AutoCodesAdminPage() {
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mt: 2, textAlign: "center" }}
|
||||
>
|
||||
Questo e il codice che verra generato alla prossima creazione.
|
||||
<br />
|
||||
La sequenza non e stata incrementata.
|
||||
{t("autoCodes.previewText")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setPreviewCode(null)}>Chiudi</Button>
|
||||
<Button onClick={() => setPreviewCode(null)}>{t("common.close")}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -427,15 +427,14 @@ export default function AutoCodesAdminPage() {
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Guida ai Pattern</DialogTitle>
|
||||
<DialogTitle>{t("autoCodes.helpTitle")}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography variant="body2" paragraph>
|
||||
I pattern definiscono come vengono generati i codici automatici.
|
||||
Puoi combinare testo statico e placeholder dinamici.
|
||||
{t("autoCodes.helpText")}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
||||
Placeholder Disponibili
|
||||
{t("autoCodes.placeholders")}
|
||||
</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ mb: 3 }}>
|
||||
<Table size="small">
|
||||
@@ -467,7 +466,7 @@ export default function AutoCodesAdminPage() {
|
||||
</TableContainer>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Esempi di Pattern
|
||||
{t("autoCodes.examples")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
@@ -533,7 +532,7 @@ export default function AutoCodesAdminPage() {
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowHelp(false)}>Chiudi</Button>
|
||||
<Button onClick={() => setShowHelp(false)}>{t("common.close")}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
@@ -548,6 +547,7 @@ interface EditConfigDialogProps {
|
||||
onSave: (data: AutoCodeUpdateDto) => void;
|
||||
isSaving: boolean;
|
||||
error: Error | null;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
function EditConfigDialog({
|
||||
@@ -557,6 +557,7 @@ function EditConfigDialog({
|
||||
onSave,
|
||||
isSaving,
|
||||
error,
|
||||
t,
|
||||
}: EditConfigDialogProps) {
|
||||
const [formData, setFormData] = useState<AutoCodeUpdateDto>({});
|
||||
|
||||
@@ -610,33 +611,33 @@ function EditConfigDialog({
|
||||
{config && (
|
||||
<>
|
||||
<DialogTitle>
|
||||
Modifica Configurazione: {config.entityName}
|
||||
{t("autoCodes.editTitle")}: {config.entityName}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Prefisso"
|
||||
label={t("autoCodes.prefix")}
|
||||
value={formData.prefix ?? config.prefix ?? ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, prefix: e.target.value || null })
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Testo sostituito nel placeholder {PREFIX}"
|
||||
helperText={t("autoCodes.prefixHelper")}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Pattern"
|
||||
label={t("autoCodes.pattern")}
|
||||
value={formData.pattern ?? config.pattern}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, pattern: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Pattern per generazione codice"
|
||||
helperText={t("autoCodes.patternHelper")}
|
||||
InputProps={{
|
||||
sx: { fontFamily: "monospace" },
|
||||
endAdornment: (
|
||||
@@ -674,7 +675,7 @@ function EditConfigDialog({
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Anteprima:
|
||||
{t("autoCodes.previewLabel")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
@@ -687,18 +688,18 @@ function EditConfigDialog({
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Reset Sequenza</InputLabel>
|
||||
<InputLabel>{t("autoCodes.resetSequence")}</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
(formData.resetSequenceMonthly ??
|
||||
config.resetSequenceMonthly)
|
||||
config.resetSequenceMonthly)
|
||||
? "monthly"
|
||||
: (formData.resetSequenceYearly ??
|
||||
config.resetSequenceYearly)
|
||||
config.resetSequenceYearly)
|
||||
? "yearly"
|
||||
: "never"
|
||||
}
|
||||
label="Reset Sequenza"
|
||||
label={t("autoCodes.resetSequence")}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFormData({
|
||||
@@ -708,9 +709,9 @@ function EditConfigDialog({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value="never">Mai</MenuItem>
|
||||
<MenuItem value="yearly">Ogni anno</MenuItem>
|
||||
<MenuItem value="monthly">Ogni mese</MenuItem>
|
||||
<MenuItem value="never">{t("autoCodes.never")}</MenuItem>
|
||||
<MenuItem value="yearly">{t("autoCodes.everyYear")}</MenuItem>
|
||||
<MenuItem value="monthly">{t("autoCodes.everyMonth")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
@@ -745,7 +746,7 @@ function EditConfigDialog({
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Generazione attiva"
|
||||
label={t("autoCodes.generationActive")}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -762,19 +763,19 @@ function EditConfigDialog({
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Codice non modificabile"
|
||||
label={t("autoCodes.readOnly")}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error.message || "Errore durante il salvataggio"}
|
||||
{error.message || t("common.error")}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Annulla</Button>
|
||||
<Button onClick={onClose}>{t("common.cancel")}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
@@ -783,7 +784,7 @@ function EditConfigDialog({
|
||||
isSaving ? <CircularProgress size={16} color="inherit" /> : null
|
||||
}
|
||||
>
|
||||
Salva
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
|
||||
@@ -17,10 +17,12 @@ import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
|
||||
export default function CalendarioPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: dayjs().startOf("month").format("YYYY-MM-DD"),
|
||||
end: dayjs().endOf("month").format("YYYY-MM-DD"),
|
||||
@@ -73,7 +75,7 @@ export default function CalendarioPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Calendario Eventi
|
||||
{t("calendar.title")}
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ p: 2 }}>
|
||||
@@ -85,7 +87,7 @@ export default function CalendarioPage() {
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||
}}
|
||||
locale="it"
|
||||
locale={i18n.language}
|
||||
events={eventi.map((e) => ({
|
||||
id: String(e.id),
|
||||
title: e.title,
|
||||
@@ -109,27 +111,27 @@ export default function CalendarioPage() {
|
||||
meridiem: false,
|
||||
}}
|
||||
buttonText={{
|
||||
today: "Oggi",
|
||||
month: "Mese",
|
||||
week: "Settimana",
|
||||
day: "Giorno",
|
||||
today: t("calendar.today"),
|
||||
month: t("calendar.month"),
|
||||
week: t("calendar.week"),
|
||||
day: t("calendar.day"),
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Dialog per creazione nuovo evento */}
|
||||
<Dialog open={newEventDialog.open} onClose={handleCloseDialog}>
|
||||
<DialogTitle>Nuovo Evento</DialogTitle>
|
||||
<DialogTitle>{t("calendar.newEvent")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Vuoi creare un nuovo evento per il giorno{" "}
|
||||
{t("calendar.createEventConfirm")}{" "}
|
||||
<strong>{newEventDialog.formattedDate}</strong>?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t("common.cancel")}</Button>
|
||||
<Button onClick={handleCreateEvent} variant="contained" autoFocus>
|
||||
Crea Evento
|
||||
{t("calendar.createEvent")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { clientiService } from "../services/lookupService";
|
||||
import { Cliente } from "../types";
|
||||
import { CustomFieldsRenderer } from "../components/customFields/CustomFieldsRenderer";
|
||||
@@ -26,6 +27,7 @@ import { CustomFieldValues } from "../types/customFields";
|
||||
|
||||
export default function ClientiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Cliente>>({ attivo: true });
|
||||
@@ -94,22 +96,22 @@ export default function ClientiPage() {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: "codice", headerName: "Codice", width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
|
||||
{ field: "codice", headerName: t("clients.code"), width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: t("clients.altCode"), width: 100 },
|
||||
{
|
||||
field: "ragioneSociale",
|
||||
headerName: "Ragione Sociale",
|
||||
headerName: t("clients.businessName"),
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
},
|
||||
{ field: "citta", headerName: "Città", width: 150 },
|
||||
{ field: "provincia", headerName: "Prov.", width: 80 },
|
||||
{ field: "telefono", headerName: "Telefono", width: 130 },
|
||||
{ field: "email", headerName: "Email", width: 200 },
|
||||
{ field: "partitaIva", headerName: "P.IVA", width: 130 },
|
||||
{ field: "citta", headerName: t("clients.city"), width: 150 },
|
||||
{ field: "provincia", headerName: t("clients.province"), width: 80 },
|
||||
{ field: "telefono", headerName: t("clients.phone"), width: 130 },
|
||||
{ field: "email", headerName: t("clients.email"), width: 200 },
|
||||
{ field: "partitaIva", headerName: t("clients.vat"), width: 130 },
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Azioni",
|
||||
headerName: t("common.actions"),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -121,7 +123,7 @@ export default function ClientiPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm("Eliminare questo cliente?")) {
|
||||
if (confirm(t("clients.deleteConfirm"))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -143,13 +145,13 @@ export default function ClientiPage() {
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Clienti</Typography>
|
||||
<Typography variant="h4">{t("clients.title")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpenDialog(true)}
|
||||
>
|
||||
Nuovo Cliente
|
||||
{t("clients.newClient")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -173,24 +175,24 @@ export default function ClientiPage() {
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{editingId ? "Modifica Cliente" : "Nuovo Cliente"}
|
||||
{editingId ? t("clients.editClient") : t("clients.newClient")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" flexWrap="wrap" gap={2} mt={1}>
|
||||
<Box flexBasis={{ xs: '100%', md: '23%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Codice"
|
||||
label={t("clients.code")}
|
||||
fullWidth
|
||||
value={
|
||||
editingId
|
||||
? formData.codice || ""
|
||||
: "(Generato al salvataggio)"
|
||||
: t("clients.generatedOnSave")
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingId
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
? t("clients.autoGenerated")
|
||||
: t("clients.willBeAssigned")
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -209,7 +211,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '23%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Codice Alternativo"
|
||||
label={t("clients.altCode")}
|
||||
fullWidth
|
||||
value={formData.codiceAlternativo || ""}
|
||||
onChange={(e) =>
|
||||
@@ -218,12 +220,12 @@ export default function ClientiPage() {
|
||||
codiceAlternativo: e.target.value,
|
||||
})
|
||||
}
|
||||
helperText="Opzionale"
|
||||
helperText={t("common.optional")}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Ragione Sociale"
|
||||
label={t("clients.businessName")}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.ragioneSociale || ""}
|
||||
@@ -234,7 +236,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '65%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Indirizzo"
|
||||
label={t("clients.address")}
|
||||
fullWidth
|
||||
value={formData.indirizzo || ""}
|
||||
onChange={(e) =>
|
||||
@@ -244,7 +246,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '32%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="CAP"
|
||||
label={t("clients.zip")}
|
||||
fullWidth
|
||||
value={formData.cap || ""}
|
||||
onChange={(e) =>
|
||||
@@ -254,7 +256,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '65%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Città"
|
||||
label={t("clients.city")}
|
||||
fullWidth
|
||||
value={formData.citta || ""}
|
||||
onChange={(e) =>
|
||||
@@ -264,7 +266,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '32%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Provincia"
|
||||
label={t("clients.province")}
|
||||
fullWidth
|
||||
value={formData.provincia || ""}
|
||||
onChange={(e) =>
|
||||
@@ -274,7 +276,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Telefono"
|
||||
label={t("clients.phone")}
|
||||
fullWidth
|
||||
value={formData.telefono || ""}
|
||||
onChange={(e) =>
|
||||
@@ -284,7 +286,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Email"
|
||||
label={t("clients.email")}
|
||||
fullWidth
|
||||
type="email"
|
||||
value={formData.email || ""}
|
||||
@@ -295,7 +297,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="PEC"
|
||||
label={t("clients.pec")}
|
||||
fullWidth
|
||||
value={formData.pec || ""}
|
||||
onChange={(e) =>
|
||||
@@ -305,7 +307,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Codice Fiscale"
|
||||
label={t("clients.fiscalCode")}
|
||||
fullWidth
|
||||
value={formData.codiceFiscale || ""}
|
||||
onChange={(e) =>
|
||||
@@ -315,7 +317,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Partita IVA"
|
||||
label={t("clients.vat")}
|
||||
fullWidth
|
||||
value={formData.partitaIva || ""}
|
||||
onChange={(e) =>
|
||||
@@ -325,7 +327,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: '48%' }} flexGrow={1}>
|
||||
<TextField
|
||||
label="Codice Destinatario"
|
||||
label={t("clients.recipientCode")}
|
||||
fullWidth
|
||||
value={formData.codiceDestinatario || ""}
|
||||
onChange={(e) =>
|
||||
@@ -338,7 +340,7 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
<Box flexBasis="100%">
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("common.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
@@ -358,9 +360,9 @@ export default function ClientiPage() {
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t("common.cancel")}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? "Salva" : "Crea"}
|
||||
{editingId ? t("common.save") : t("common.create")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,302 +1,627 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
Button,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Snackbar,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { CustomFieldDefinition, CustomFieldType } from '../types/customFields';
|
||||
import { customFieldService } from '../services/customFieldService';
|
||||
TextField,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {
|
||||
Refresh as RefreshIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Add as AddIcon,
|
||||
} from "@mui/icons-material";
|
||||
import * as Icons from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { customFieldService } from "../services/customFieldService";
|
||||
import {
|
||||
CustomFieldDefinition,
|
||||
CustomFieldType,
|
||||
groupByEntity,
|
||||
entityNames,
|
||||
entityIcons,
|
||||
fieldTypeNames,
|
||||
} from "../types/customFields";
|
||||
|
||||
const ENTITIES = [
|
||||
{ value: 'Cliente', label: 'Clienti' },
|
||||
{ value: 'Articolo', label: 'Articoli (Catering)' },
|
||||
{ value: 'Evento', label: 'Eventi' },
|
||||
{ value: 'WarehouseArticle', label: 'Articoli Magazzino' },
|
||||
{ value: 'WarehouseLocation', label: 'Magazzini' },
|
||||
{ value: 'Risorsa', label: 'Risorse (Staff)' }
|
||||
];
|
||||
export default function CustomFieldsAdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [editingConfig, setEditingConfig] = useState<CustomFieldDefinition | null>(
|
||||
null,
|
||||
);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState<number | null>(null);
|
||||
const [expandedEntity, setExpandedEntity] = useState<string | false>("Client");
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ value: CustomFieldType.Text, label: 'Testo' },
|
||||
{ value: CustomFieldType.Number, label: 'Numero' },
|
||||
{ value: CustomFieldType.Date, label: 'Data' },
|
||||
{ value: CustomFieldType.Boolean, label: 'Booleano (Sì/No)' },
|
||||
{ value: CustomFieldType.Select, label: 'Lista a discesa' },
|
||||
{ value: CustomFieldType.TextArea, label: 'Area di testo' },
|
||||
{ value: CustomFieldType.Color, label: 'Colore' },
|
||||
{ value: CustomFieldType.Url, label: 'URL' },
|
||||
{ value: CustomFieldType.Email, label: 'Email' }
|
||||
];
|
||||
|
||||
const CustomFieldsAdminPage: React.FC = () => {
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>(ENTITIES[0].value);
|
||||
const [fields, setFields] = useState<CustomFieldDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentField, setCurrentField] = useState<Partial<CustomFieldDefinition>>({});
|
||||
const [snackbar, setSnackbar] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success'
|
||||
// Query per tutte le configurazioni
|
||||
const {
|
||||
data: configs = [],
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["customFields"],
|
||||
queryFn: () => customFieldService.getAll(),
|
||||
});
|
||||
|
||||
const showSnackbar = (message: string, severity: 'success' | 'error') => {
|
||||
setSnackbar({ open: true, message, severity });
|
||||
// Mutation per creare configurazione
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Omit<CustomFieldDefinition, "id">) =>
|
||||
customFieldService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["customFields"] });
|
||||
setIsCreating(false);
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation per aggiornare configurazione
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: number;
|
||||
data: CustomFieldDefinition;
|
||||
}) => customFieldService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["customFields"] });
|
||||
setEditingConfig(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation per eliminare configurazione
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => customFieldService.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["customFields"] });
|
||||
setConfirmDelete(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Raggruppa configurazioni per entità
|
||||
const groupedConfigs = groupByEntity(configs);
|
||||
|
||||
// Helper per ottenere icona entità
|
||||
const getEntityIcon = (entityCode: string) => {
|
||||
const iconName = entityIcons[entityCode] || "Extension";
|
||||
const IconComponent = (Icons as Record<string, React.ComponentType>)[
|
||||
iconName
|
||||
];
|
||||
return IconComponent ? <IconComponent /> : <Icons.Extension />;
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => setSnackbar({ ...snackbar, open: false });
|
||||
|
||||
const loadFields = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await customFieldService.getByEntity(selectedEntity);
|
||||
setFields(data);
|
||||
} catch (error) {
|
||||
showSnackbar('Errore nel caricamento dei campi', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadFields();
|
||||
}, [selectedEntity]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (currentField.id) {
|
||||
await customFieldService.update(currentField.id, currentField as CustomFieldDefinition);
|
||||
showSnackbar('Campo aggiornato con successo', 'success');
|
||||
} else {
|
||||
await customFieldService.create({
|
||||
...currentField,
|
||||
entityName: selectedEntity,
|
||||
isActive: true,
|
||||
sortOrder: fields.length + 1
|
||||
} as CustomFieldDefinition);
|
||||
showSnackbar('Campo creato con successo', 'success');
|
||||
}
|
||||
setDialogOpen(false);
|
||||
loadFields();
|
||||
} catch (error) {
|
||||
showSnackbar('Errore nel salvataggio', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm('Sei sicuro di voler eliminare questo campo?')) {
|
||||
try {
|
||||
await customFieldService.delete(id);
|
||||
showSnackbar('Campo eliminato', 'success');
|
||||
loadFields();
|
||||
} catch (error) {
|
||||
showSnackbar('Errore durante l\'eliminazione', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'label', headerName: 'Etichetta', flex: 1 },
|
||||
{ field: 'fieldName', headerName: 'Nome Interno', flex: 1 },
|
||||
{
|
||||
field: 'type',
|
||||
headerName: 'Tipo',
|
||||
width: 150,
|
||||
valueFormatter: (params) => FIELD_TYPES.find(t => t.value === params.value)?.label
|
||||
},
|
||||
{
|
||||
field: 'isRequired',
|
||||
headerName: 'Obbligatorio',
|
||||
width: 120,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
field: 'sortOrder',
|
||||
headerName: 'Ordine',
|
||||
width: 100,
|
||||
type: 'number',
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
headerName: 'Azioni',
|
||||
width: 100,
|
||||
getActions: (params) => [
|
||||
<GridActionsCellItem
|
||||
icon={<EditIcon />}
|
||||
label="Modifica"
|
||||
onClick={() => {
|
||||
setCurrentField(params.row);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<DeleteIcon />}
|
||||
label="Elimina"
|
||||
onClick={() => handleDelete(params.row.id)}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
];
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Gestione Campi Personalizzati
|
||||
</Typography>
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 3,
|
||||
flexWrap: "wrap",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{t("customFields.title")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
{t("customFields.title")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setIsCreating(true)}
|
||||
>
|
||||
{t("customFields.newField")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box display="flex" flexWrap="wrap" gap={2} alignItems="center">
|
||||
<Box flexBasis={{ xs: '100%', md: '33%' }} flexGrow={1}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Entità</InputLabel>
|
||||
<Select
|
||||
value={selectedEntity}
|
||||
label="Entità"
|
||||
onChange={(e) => setSelectedEntity(e.target.value)}
|
||||
{/* Accordion per entità */}
|
||||
{Object.entries(entityNames).map(([entityCode, entityName]) => {
|
||||
const entityConfigs = groupedConfigs[entityCode] || [];
|
||||
return (
|
||||
<Accordion
|
||||
key={entityCode}
|
||||
expanded={expandedEntity === entityCode}
|
||||
onChange={(_, isExpanded) =>
|
||||
setExpandedEntity(isExpanded ? entityCode : false)
|
||||
}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{getEntityIcon(entityCode)}
|
||||
<Typography variant="h6">
|
||||
{t(`customFields.entities.${entityCode.toLowerCase()}`) || entityName}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${entityConfigs.length} campi`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{entityConfigs.length === 0 ? (
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
sx={{ py: 2 }}
|
||||
>
|
||||
{ENTITIES.map(e => (
|
||||
<MenuItem key={e.value} value={e.value}>{e.label}</MenuItem>
|
||||
{t("customFields.noFields")}
|
||||
</Typography>
|
||||
) : (
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("customFields.label")}</TableCell>
|
||||
<TableCell>{t("customFields.fieldName")}</TableCell>
|
||||
<TableCell>{t("customFields.type")}</TableCell>
|
||||
<TableCell align="center">
|
||||
{t("customFields.required")}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{t("customFields.order")}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{t("autoCodes.status")}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{t("common.actions")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{entityConfigs.map((config) => (
|
||||
<TableRow key={config.id} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{config.label}
|
||||
</Typography>
|
||||
{config.description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
>
|
||||
{config.description}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace" }}
|
||||
>
|
||||
{config.fieldName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={t(`customFields.types.${CustomFieldType[config.type].toLowerCase()}`)}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{config.isRequired ? (
|
||||
<Chip
|
||||
label={t("autoCodes.yes")}
|
||||
size="small"
|
||||
color="warning"
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
label={t("autoCodes.no")}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{config.sortOrder}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={
|
||||
config.isActive
|
||||
? t("modules.admin.active")
|
||||
: t("modules.admin.inactive")
|
||||
}
|
||||
size="small"
|
||||
color={config.isActive ? "success" : "default"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title={t("common.edit")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingConfig(config)}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("common.delete")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setConfirmDelete(config.id)}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Dialog creazione/modifica */}
|
||||
<EditConfigDialog
|
||||
config={editingConfig}
|
||||
isCreating={isCreating}
|
||||
onClose={() => {
|
||||
setEditingConfig(null);
|
||||
setIsCreating(false);
|
||||
}}
|
||||
onSave={(data) => {
|
||||
if (isCreating) {
|
||||
createMutation.mutate(data as Omit<CustomFieldDefinition, "id">);
|
||||
} else if (editingConfig) {
|
||||
updateMutation.mutate({
|
||||
id: editingConfig.id,
|
||||
data: data as CustomFieldDefinition,
|
||||
});
|
||||
}
|
||||
}}
|
||||
isSaving={createMutation.isPending || updateMutation.isPending}
|
||||
error={
|
||||
(createMutation.error || updateMutation.error) as Error | null
|
||||
}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Dialog conferma eliminazione */}
|
||||
<Dialog
|
||||
open={!!confirmDelete}
|
||||
onClose={() => setConfirmDelete(null)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>{t("common.confirmDelete")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>{t("customFields.deleteConfirm")}</Typography>
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
{t("common.irreversibleAction")}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDelete(null)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
confirmDelete && deleteMutation.mutate(confirmDelete)
|
||||
}
|
||||
disabled={deleteMutation.isPending}
|
||||
startIcon={
|
||||
deleteMutation.isPending ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
<DeleteIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog per creazione/modifica configurazione
|
||||
interface EditConfigDialogProps {
|
||||
config: CustomFieldDefinition | null;
|
||||
isCreating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (
|
||||
data: Omit<CustomFieldDefinition, "id"> | CustomFieldDefinition,
|
||||
) => void;
|
||||
isSaving: boolean;
|
||||
error: Error | null;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
function EditConfigDialog({
|
||||
config,
|
||||
isCreating,
|
||||
onClose,
|
||||
onSave,
|
||||
isSaving,
|
||||
error,
|
||||
t,
|
||||
}: EditConfigDialogProps) {
|
||||
const [formData, setFormData] = useState<Partial<CustomFieldDefinition>>({});
|
||||
|
||||
// Reset form quando apre
|
||||
const handleOpen = () => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
entityName: config.entityName,
|
||||
fieldName: config.fieldName,
|
||||
label: config.label,
|
||||
type: config.type,
|
||||
isRequired: config.isRequired,
|
||||
optionsJson: config.optionsJson,
|
||||
defaultValue: config.defaultValue,
|
||||
description: config.description,
|
||||
sortOrder: config.sortOrder,
|
||||
isActive: config.isActive,
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
entityName: "Client",
|
||||
type: CustomFieldType.Text,
|
||||
isRequired: false,
|
||||
isActive: true,
|
||||
sortOrder: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(formData as Omit<CustomFieldDefinition, "id">);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!config || isCreating}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
TransitionProps={{ onEntered: handleOpen }}
|
||||
>
|
||||
<DialogTitle>
|
||||
{isCreating
|
||||
? t("customFields.newField")
|
||||
: `${t("customFields.editField")}: ${config?.label}`}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
{isCreating && (
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>{t("customFields.entity")}</InputLabel>
|
||||
<Select
|
||||
value={formData.entityName || "Client"}
|
||||
label={t("customFields.entity")}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, entityName: e.target.value })
|
||||
}
|
||||
>
|
||||
{Object.entries(entityNames).map(([code, name]) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{t(`customFields.entities.${code.toLowerCase()}`) || name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box flexBasis={{ xs: '100%', md: 'auto' }} display="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => {
|
||||
setCurrentField({
|
||||
type: CustomFieldType.Text,
|
||||
isRequired: false,
|
||||
entityName: selectedEntity
|
||||
});
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Nuovo Campo
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<div style={{ height: 600, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={fields}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{currentField.id ? 'Modifica Campo' : 'Nuovo Campo'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Etichetta (Label)"
|
||||
label={t("customFields.label")}
|
||||
value={formData.label || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, label: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
value={currentField.label || ''}
|
||||
onChange={(e) => setCurrentField({ ...currentField, label: e.target.value })}
|
||||
size="small"
|
||||
required
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Nome Interno (es. data_nascita)"
|
||||
label={t("customFields.fieldName")}
|
||||
value={formData.fieldName || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, fieldName: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
value={currentField.fieldName || ''}
|
||||
onChange={(e) => setCurrentField({ ...currentField, fieldName: e.target.value })}
|
||||
size="small"
|
||||
required
|
||||
helperText="Deve essere univoco per l'entità. Usa solo lettere minuscole e underscore."
|
||||
disabled={!!currentField.id}
|
||||
disabled={!isCreating}
|
||||
helperText={isCreating ? t("customFields.fieldNameHelper") : ""}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo</InputLabel>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>{t("customFields.type")}</InputLabel>
|
||||
<Select
|
||||
value={currentField.type}
|
||||
label="Tipo"
|
||||
onChange={(e) => setCurrentField({ ...currentField, type: e.target.value as CustomFieldType })}
|
||||
value={formData.type ?? CustomFieldType.Text}
|
||||
label={t("customFields.type")}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
type: Number(e.target.value) as CustomFieldType,
|
||||
})
|
||||
}
|
||||
disabled={!isCreating}
|
||||
>
|
||||
{FIELD_TYPES.map(t => (
|
||||
<MenuItem key={t.value} value={t.value}>{t.label}</MenuItem>
|
||||
))}
|
||||
{Object.entries(fieldTypeNames).map(([type]) => {
|
||||
const typeEnum = Number(type) as CustomFieldType;
|
||||
return (
|
||||
<MenuItem key={type} value={type}>
|
||||
{t(`customFields.types.${CustomFieldType[typeEnum].toLowerCase()}`)}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{currentField.type === CustomFieldType.Select && (
|
||||
<TextField
|
||||
label="Opzioni (JSON Array)"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={currentField.optionsJson || ''}
|
||||
onChange={(e) => setCurrentField({ ...currentField, optionsJson: e.target.value })}
|
||||
placeholder='["Opzione 1", "Opzione 2"]'
|
||||
helperText="Inserisci un array JSON valido di stringhe"
|
||||
/>
|
||||
{(formData.type === CustomFieldType.Select ||
|
||||
formData.type === CustomFieldType.MultiSelect) && (
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label={t("customFields.optionsJson")}
|
||||
value={formData.optionsJson || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, optionsJson: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
multiline
|
||||
rows={3}
|
||||
helperText={t("customFields.optionsHelper")}
|
||||
InputProps={{ sx: { fontFamily: "monospace" } }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Descrizione / Helper Text"
|
||||
label={t("customFields.description")}
|
||||
value={formData.description || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
value={currentField.description || ''}
|
||||
onChange={(e) => setCurrentField({ ...currentField, description: e.target.value })}
|
||||
size="small"
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("customFields.order")}
|
||||
type="number"
|
||||
value={formData.sortOrder || 0}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
sortOrder: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={currentField.isRequired || false}
|
||||
onChange={(e) => setCurrentField({ ...currentField, isRequired: e.target.checked })}
|
||||
<Switch
|
||||
checked={formData.isRequired || false}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, isRequired: e.target.checked })
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Obbligatorio"
|
||||
label={t("customFields.required")}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
label="Ordine"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={currentField.sortOrder || 0}
|
||||
onChange={(e) => setCurrentField({ ...currentField, sortOrder: parseInt(e.target.value) })}
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formData.isActive !== false}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, isActive: e.target.checked })
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("modules.admin.active")}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)}>Annulla</Button>
|
||||
<Button onClick={handleSave} variant="contained">Salva</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Snackbar open={snackbar.open} autoHideDuration={6000} onClose={handleCloseSnackbar}>
|
||||
<Alert onClose={handleCloseSnackbar} severity={snackbar.severity} sx={{ width: '100%' }}>
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error.message || t("common.error")}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t("common.cancel")}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
startIcon={
|
||||
isSaving ? <CircularProgress size={16} color="inherit" /> : null
|
||||
}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomFieldsAdminPage;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { demoService, DemoDataResult } from "../services/demoService";
|
||||
import { StatoEvento } from "../types";
|
||||
@@ -66,16 +67,16 @@ const StatCard = ({
|
||||
</Card>
|
||||
);
|
||||
|
||||
const getStatoLabel = (stato: StatoEvento) => {
|
||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return "Scheda";
|
||||
return t("status.scheda");
|
||||
case StatoEvento.Preventivo:
|
||||
return "Preventivo";
|
||||
return t("status.preventivo");
|
||||
case StatoEvento.Confermato:
|
||||
return "Confermato";
|
||||
return t("status.confermato");
|
||||
default:
|
||||
return "Sconosciuto";
|
||||
return t("common.unknown");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,6 +96,7 @@ const getStatoColor = (stato: StatoEvento) => {
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [demoDialog, setDemoDialog] = useState<"generate" | "clear" | null>(
|
||||
null,
|
||||
);
|
||||
@@ -116,7 +118,7 @@ export default function Dashboard() {
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || "Errore durante la generazione dei dati",
|
||||
err.response?.data?.error || t("dashboard.generateError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -132,7 +134,7 @@ export default function Dashboard() {
|
||||
queryClient.invalidateQueries();
|
||||
} catch (err: any) {
|
||||
setError(
|
||||
err.response?.data?.error || "Errore durante la pulizia dei dati",
|
||||
err.response?.data?.error || t("dashboard.clearError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -176,7 +178,7 @@ export default function Dashboard() {
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Dashboard</Typography>
|
||||
<Typography variant="h4">{t("dashboard.title")}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -184,7 +186,7 @@ export default function Dashboard() {
|
||||
startIcon={<GenerateIcon />}
|
||||
onClick={() => setDemoDialog("generate")}
|
||||
>
|
||||
Genera Dati Demo
|
||||
{t("dashboard.generateDemoData")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -192,7 +194,7 @@ export default function Dashboard() {
|
||||
startIcon={<ClearIcon />}
|
||||
onClick={() => setDemoDialog("clear")}
|
||||
>
|
||||
Pulisci Database
|
||||
{t("dashboard.clearDatabase")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -200,7 +202,7 @@ export default function Dashboard() {
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Eventi Totali"
|
||||
title={t("dashboard.totalEvents")}
|
||||
value={eventi.length}
|
||||
icon={<EventIcon sx={{ fontSize: 48 }} />}
|
||||
color="#1976d2"
|
||||
@@ -208,7 +210,7 @@ export default function Dashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Confermati"
|
||||
title={t("dashboard.confirmed")}
|
||||
value={eventiConfermati}
|
||||
icon={<ConfirmedIcon sx={{ fontSize: 48 }} />}
|
||||
color="#4caf50"
|
||||
@@ -216,7 +218,7 @@ export default function Dashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="In Preventivo"
|
||||
title={t("dashboard.inQuote")}
|
||||
value={eventiPreventivo}
|
||||
icon={<PendingIcon sx={{ fontSize: 48 }} />}
|
||||
color="#ff9800"
|
||||
@@ -224,7 +226,7 @@ export default function Dashboard() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard
|
||||
title="Eventi Oggi"
|
||||
title={t("dashboard.eventsToday")}
|
||||
value={eventiOggi}
|
||||
icon={<PeopleIcon sx={{ fontSize: 48 }} />}
|
||||
color="#9c27b0"
|
||||
@@ -236,7 +238,7 @@ export default function Dashboard() {
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Prossimi Eventi (30 giorni)
|
||||
{t("dashboard.upcomingEvents")}
|
||||
</Typography>
|
||||
<List>
|
||||
{eventiProssimi.slice(0, 10).map((evento) => (
|
||||
@@ -265,7 +267,7 @@ export default function Dashboard() {
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label={getStatoLabel(evento.stato)}
|
||||
label={getStatoLabel(evento.stato, t)}
|
||||
color={getStatoColor(evento.stato) as any}
|
||||
size="small"
|
||||
/>
|
||||
@@ -273,7 +275,7 @@ export default function Dashboard() {
|
||||
))}
|
||||
{eventiProssimi.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary="Nessun evento nei prossimi 30 giorni" />
|
||||
<ListItemText primary={t("dashboard.noEvents")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
@@ -283,7 +285,7 @@ export default function Dashboard() {
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Preventivi in Scadenza
|
||||
{t("dashboard.expiringQuotes")}
|
||||
</Typography>
|
||||
<List>
|
||||
{eventi
|
||||
@@ -310,10 +312,10 @@ export default function Dashboard() {
|
||||
>
|
||||
<ListItemText
|
||||
primary={evento.descrizione || evento.codice}
|
||||
secondary={`Scade: ${dayjs(evento.dataScadenzaPreventivo).format("DD/MM/YYYY")}`}
|
||||
secondary={t("dashboard.expires", { date: dayjs(evento.dataScadenzaPreventivo).format("DD/MM/YYYY") })}
|
||||
/>
|
||||
<Chip
|
||||
label={`${evento.numeroOspiti || 0} ospiti`}
|
||||
label={t("dashboard.guests", { count: evento.numeroOspiti || 0 })}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
@@ -321,10 +323,10 @@ export default function Dashboard() {
|
||||
))}
|
||||
{eventi.filter((e) => e.stato === StatoEvento.Preventivo)
|
||||
.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText primary="Nessun preventivo in attesa" />
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemText primary={t("dashboard.noQuotes")} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
@@ -332,18 +334,11 @@ export default function Dashboard() {
|
||||
|
||||
{/* Dialog Genera Dati Demo */}
|
||||
<Dialog open={demoDialog === "generate"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>Genera Dati Demo</DialogTitle>
|
||||
<DialogTitle>{t("dashboard.generateDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
Questa operazione genera dati di test per dimostrazioni:
|
||||
<br />- 15 Clienti
|
||||
<br />- 10 Location
|
||||
<br />- 12 Risorse (staff)
|
||||
<br />- 20 Articoli
|
||||
<br />- 20 Eventi con dettagli
|
||||
<br />
|
||||
<br />I dati esistenti non verranno modificati.
|
||||
<Trans i18nKey="dashboard.generateDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
@@ -364,7 +359,7 @@ export default function Dashboard() {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? "Chiudi" : "Annulla"}
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
@@ -372,7 +367,7 @@ export default function Dashboard() {
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
>
|
||||
Genera
|
||||
{t("common.generate")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
@@ -380,24 +375,16 @@ export default function Dashboard() {
|
||||
|
||||
{/* Dialog Pulisci Database */}
|
||||
<Dialog open={demoDialog === "clear"} onClose={handleCloseDialog}>
|
||||
<DialogTitle>Pulisci Database</DialogTitle>
|
||||
<DialogTitle>{t("dashboard.clearDialogTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{!result && !error && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Attenzione: questa operazione elimina TUTTI i dati dal database!
|
||||
{t("dashboard.clearDialogWarning")}
|
||||
</Alert>
|
||||
)}
|
||||
{!result && !error && (
|
||||
<DialogContentText>
|
||||
Verranno eliminati:
|
||||
<br />- Tutti gli eventi e i relativi dettagli
|
||||
<br />- Tutti i clienti
|
||||
<br />- Tutte le location
|
||||
<br />- Tutte le risorse
|
||||
<br />- Tutti gli articoli
|
||||
<br />
|
||||
<br />
|
||||
Questa operazione non puo essere annullata.
|
||||
<Trans i18nKey="dashboard.clearDialogText" components={{ br: <br /> }} />
|
||||
</DialogContentText>
|
||||
)}
|
||||
{loading && (
|
||||
@@ -407,9 +394,13 @@ export default function Dashboard() {
|
||||
)}
|
||||
{result && (
|
||||
<Alert severity="success" sx={{ mt: 1 }}>
|
||||
Database pulito. Eliminati: {result.eventiCreati} eventi,{" "}
|
||||
{result.clientiCreati} clienti, {result.locationCreate} location,{" "}
|
||||
{result.risorseCreate} risorse, {result.articoliCreati} articoli.
|
||||
{t("dashboard.clearSuccess", {
|
||||
events: result.eventiCreati,
|
||||
clients: result.clientiCreati,
|
||||
locations: result.locationCreate,
|
||||
resources: result.risorseCreate,
|
||||
articles: result.articoliCreati
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
@@ -420,7 +411,7 @@ export default function Dashboard() {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>
|
||||
{result ? "Chiudi" : "Annulla"}
|
||||
{result ? t("common.close") : t("common.cancel")}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
@@ -429,7 +420,7 @@ export default function Dashboard() {
|
||||
color="error"
|
||||
disabled={loading}
|
||||
>
|
||||
Elimina Tutto
|
||||
{t("common.deleteAll")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
|
||||
@@ -28,16 +28,17 @@ import {
|
||||
Visibility as ViewIcon,
|
||||
} from '@mui/icons-material';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventiService } from '../services/eventiService';
|
||||
import { lookupService } from '../services/lookupService';
|
||||
import { Evento, StatoEvento } from '../types';
|
||||
|
||||
const getStatoLabel = (stato: StatoEvento) => {
|
||||
const getStatoLabel = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda: return 'Scheda';
|
||||
case StatoEvento.Preventivo: return 'Preventivo';
|
||||
case StatoEvento.Confermato: return 'Confermato';
|
||||
default: return 'Sconosciuto';
|
||||
case StatoEvento.Scheda: return t('status.scheda');
|
||||
case StatoEvento.Preventivo: return t('status.preventivo');
|
||||
case StatoEvento.Confermato: return t('status.confermato');
|
||||
default: return t('common.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,6 +54,7 @@ const getStatoColor = (stato: StatoEvento): 'default' | 'warning' | 'success' =>
|
||||
export default function EventiPage() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<Evento>>({
|
||||
dataEvento: dayjs().format('YYYY-MM-DD'),
|
||||
@@ -103,39 +105,39 @@ export default function EventiPage() {
|
||||
});
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'codice', headerName: 'Codice', width: 120 },
|
||||
{ field: 'codice', headerName: t('events.code'), width: 120 },
|
||||
{
|
||||
field: 'dataEvento',
|
||||
headerName: 'Data',
|
||||
headerName: t('events.date'),
|
||||
width: 120,
|
||||
valueFormatter: (value: string) => dayjs(value).format('DD/MM/YYYY'),
|
||||
},
|
||||
{ field: 'descrizione', headerName: 'Descrizione', flex: 1, minWidth: 200 },
|
||||
{ field: 'descrizione', headerName: t('events.description'), flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: 'cliente',
|
||||
headerName: 'Cliente',
|
||||
headerName: t('events.client'),
|
||||
width: 180,
|
||||
valueGetter: (value: any) => value?.ragioneSociale || '',
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
headerName: 'Location',
|
||||
headerName: t('events.location'),
|
||||
width: 150,
|
||||
valueGetter: (value: any) => value?.nome || '',
|
||||
},
|
||||
{
|
||||
field: 'numeroOspiti',
|
||||
headerName: 'Ospiti',
|
||||
headerName: t('events.guests'),
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'stato',
|
||||
headerName: 'Stato',
|
||||
headerName: t('events.status'),
|
||||
width: 120,
|
||||
renderCell: (params) => (
|
||||
<Chip
|
||||
label={getStatoLabel(params.value)}
|
||||
label={getStatoLabel(params.value, t)}
|
||||
color={getStatoColor(params.value)}
|
||||
size="small"
|
||||
/>
|
||||
@@ -143,7 +145,7 @@ export default function EventiPage() {
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: 'Azioni',
|
||||
headerName: t('common.actions'),
|
||||
width: 180,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -161,7 +163,7 @@ export default function EventiPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm('Eliminare questo evento?')) {
|
||||
if (confirm(t('common.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -180,9 +182,9 @@ export default function EventiPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">Eventi</Typography>
|
||||
<Typography variant="h4">{t('events.title')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
Nuovo Evento
|
||||
{t('events.newEvent')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -201,26 +203,26 @@ export default function EventiPage() {
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Nuovo Evento</DialogTitle>
|
||||
<DialogTitle>{t('events.newEvent')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
||||
<DatePicker
|
||||
label="Data Evento"
|
||||
label={t('events.eventDate')}
|
||||
value={formData.dataEvento ? dayjs(formData.dataEvento) : null}
|
||||
onChange={(date) => setFormData({ ...formData, dataEvento: date?.format('YYYY-MM-DD') })}
|
||||
slotProps={{ textField: { fullWidth: true } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
label={t('events.description')}
|
||||
fullWidth
|
||||
value={formData.descrizione || ''}
|
||||
onChange={(e) => setFormData({ ...formData, descrizione: e.target.value })}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Cliente</InputLabel>
|
||||
<InputLabel>{t('events.client')}</InputLabel>
|
||||
<Select
|
||||
value={formData.clienteId || ''}
|
||||
label="Cliente"
|
||||
label={t('events.client')}
|
||||
onChange={(e) => setFormData({ ...formData, clienteId: e.target.value as number })}
|
||||
>
|
||||
{clienti.map((c) => (
|
||||
@@ -229,10 +231,10 @@ export default function EventiPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Location</InputLabel>
|
||||
<InputLabel>{t('events.location')}</InputLabel>
|
||||
<Select
|
||||
value={formData.locationId || ''}
|
||||
label="Location"
|
||||
label={t('events.location')}
|
||||
onChange={(e) => setFormData({ ...formData, locationId: e.target.value as number })}
|
||||
>
|
||||
{locations.map((l) => (
|
||||
@@ -241,10 +243,10 @@ export default function EventiPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo Evento</InputLabel>
|
||||
<InputLabel>{t('events.type')}</InputLabel>
|
||||
<Select
|
||||
value={formData.tipoEventoId || ''}
|
||||
label="Tipo Evento"
|
||||
label={t('events.type')}
|
||||
onChange={(e) => setFormData({ ...formData, tipoEventoId: e.target.value as number })}
|
||||
>
|
||||
{tipiEvento.map((t) => (
|
||||
@@ -253,7 +255,7 @@ export default function EventiPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Numero Ospiti"
|
||||
label={t('events.guests')}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={formData.numeroOspiti || ''}
|
||||
@@ -262,8 +264,8 @@ export default function EventiPage() {
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenDialog(false)}>Annulla</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>Crea</Button>
|
||||
<Button onClick={() => setOpenDialog(false)}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>{t('common.create')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
CheckCircle as ConfirmIcon,
|
||||
Print as PrintIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dayjs from "dayjs";
|
||||
import { eventiService } from "../services/eventiService";
|
||||
import { lookupService } from "../services/lookupService";
|
||||
@@ -67,20 +68,21 @@ function TabPanel(props: TabPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const getStatoInfo = (stato: StatoEvento) => {
|
||||
const getStatoInfo = (stato: StatoEvento, t: any) => {
|
||||
switch (stato) {
|
||||
case StatoEvento.Scheda:
|
||||
return { label: "Scheda Evento", color: "#CAE3FC", textColor: "#1976d2" };
|
||||
return { label: t("events.detail.status.draft"), color: "#CAE3FC", textColor: "#1976d2" };
|
||||
case StatoEvento.Preventivo:
|
||||
return { label: "Preventivo", color: "#ffffb8", textColor: "#ed6c02" };
|
||||
return { label: t("events.detail.status.quote"), color: "#ffffb8", textColor: "#ed6c02" };
|
||||
case StatoEvento.Confermato:
|
||||
return { label: "Confermato", color: "#b8ffb8", textColor: "#2e7d32" };
|
||||
return { label: t("events.detail.status.confirmed"), color: "#b8ffb8", textColor: "#2e7d32" };
|
||||
default:
|
||||
return { label: "Nuovo", color: "#fafafa", textColor: "#666" };
|
||||
return { label: t("events.detail.status.new"), color: "#fafafa", textColor: "#666" };
|
||||
}
|
||||
};
|
||||
|
||||
export default function EventoDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -323,11 +325,11 @@ export default function EventoDetailPage() {
|
||||
});
|
||||
|
||||
if (isLoading && !isNew) {
|
||||
return <Typography>Caricamento...</Typography>;
|
||||
return <Typography>{t("events.detail.loading")}</Typography>;
|
||||
}
|
||||
|
||||
const data = isNew ? formData : { ...evento, ...formData };
|
||||
const statoInfo = getStatoInfo(data.stato || StatoEvento.Scheda);
|
||||
const statoInfo = getStatoInfo(data.stato || StatoEvento.Scheda, t);
|
||||
|
||||
const handleFieldChange = (field: string, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
@@ -373,8 +375,8 @@ export default function EventoDetailPage() {
|
||||
{statoInfo.label}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
{data.codice || "Nuovo Evento"} -{" "}
|
||||
{data.descrizione || "Senza descrizione"}
|
||||
{data.codice || t("events.detail.newEvent")} -{" "}
|
||||
{data.descrizione || t("events.detail.noDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
@@ -386,7 +388,7 @@ export default function EventoDetailPage() {
|
||||
onClick={() => duplicaMutation.mutate()}
|
||||
size="small"
|
||||
>
|
||||
Duplica
|
||||
{t("events.detail.actions.duplicate")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -394,7 +396,7 @@ export default function EventoDetailPage() {
|
||||
onClick={() => ricalcolaQuantitaMutation.mutate()}
|
||||
size="small"
|
||||
>
|
||||
Ricalcola Qta
|
||||
{t("events.detail.actions.recalculate")}
|
||||
</Button>
|
||||
{data.stato !== StatoEvento.Confermato && (
|
||||
<Button
|
||||
@@ -406,7 +408,7 @@ export default function EventoDetailPage() {
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
Conferma
|
||||
{t("events.detail.actions.confirm")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -417,7 +419,7 @@ export default function EventoDetailPage() {
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges && !isNew}
|
||||
>
|
||||
Salva
|
||||
{t("events.detail.actions.save")}
|
||||
</Button>
|
||||
{!isNew && (
|
||||
<Button
|
||||
@@ -430,7 +432,7 @@ export default function EventoDetailPage() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
Stampa PDF
|
||||
{t("events.detail.actions.print")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -443,7 +445,7 @@ export default function EventoDetailPage() {
|
||||
{/* Prima riga: Data, Orari, Tipo */}
|
||||
<Grid size={{ xs: 12, md: 2 }}>
|
||||
<DatePicker
|
||||
label="Data Evento"
|
||||
label={t("events.detail.fields.date")}
|
||||
value={data.dataEvento ? dayjs(data.dataEvento) : null}
|
||||
onChange={(date) =>
|
||||
handleFieldChange("dataEvento", date?.format("YYYY-MM-DD"))
|
||||
@@ -455,7 +457,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 1.5 }}>
|
||||
<TimePicker
|
||||
label="Ora Inizio"
|
||||
label={t("events.detail.fields.startTime")}
|
||||
value={
|
||||
data.oraInizio ? dayjs(`2000-01-01T${data.oraInizio}`) : null
|
||||
}
|
||||
@@ -467,7 +469,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 1.5 }}>
|
||||
<TimePicker
|
||||
label="Ora Fine"
|
||||
label={t("events.detail.fields.endTime")}
|
||||
value={data.oraFine ? dayjs(`2000-01-01T${data.oraFine}`) : null}
|
||||
onChange={(time) =>
|
||||
handleFieldChange("oraFine", time?.format("HH:mm:ss"))
|
||||
@@ -477,10 +479,10 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Tipo Evento</InputLabel>
|
||||
<InputLabel>{t("events.detail.fields.type")}</InputLabel>
|
||||
<Select
|
||||
value={data.tipoEventoId || ""}
|
||||
label="Tipo Evento"
|
||||
label={t("events.detail.fields.type")}
|
||||
onChange={(e) =>
|
||||
handleFieldChange("tipoEventoId", e.target.value)
|
||||
}
|
||||
@@ -495,12 +497,12 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Descrizione Evento"
|
||||
label={t("events.detail.fields.description")}
|
||||
fullWidth
|
||||
size="small"
|
||||
value={data.descrizione || ""}
|
||||
onChange={(e) => handleFieldChange("descrizione", e.target.value)}
|
||||
placeholder="es. Matrimonio Rossi-Bianchi"
|
||||
placeholder={t("events.detail.fields.descriptionPlaceholder")}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -514,7 +516,7 @@ export default function EventoDetailPage() {
|
||||
handleFieldChange("clienteId", newValue?.id)
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Cliente" size="small" fullWidth />
|
||||
<TextField {...params} label={t("events.detail.fields.client")} size="small" fullWidth />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -529,7 +531,7 @@ export default function EventoDetailPage() {
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Location"
|
||||
label={t("events.detail.fields.location")}
|
||||
size="small"
|
||||
fullWidth
|
||||
/>
|
||||
@@ -540,7 +542,7 @@ export default function EventoDetailPage() {
|
||||
{/* Terza riga: Dati economici */}
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<TextField
|
||||
label="N. Ospiti Totale"
|
||||
label={t("events.detail.fields.totalGuests")}
|
||||
type="number"
|
||||
fullWidth
|
||||
size="small"
|
||||
@@ -556,7 +558,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<TextField
|
||||
label="Costo a Persona"
|
||||
label={t("events.detail.fields.costPerPerson")}
|
||||
type="number"
|
||||
fullWidth
|
||||
size="small"
|
||||
@@ -574,7 +576,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<TextField
|
||||
label="Costo Totale"
|
||||
label={t("events.detail.fields.totalCost")}
|
||||
type="number"
|
||||
fullWidth
|
||||
size="small"
|
||||
@@ -592,7 +594,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<TextField
|
||||
label="Totale Acconti"
|
||||
label={t("events.detail.fields.totalDeposits")}
|
||||
fullWidth
|
||||
size="small"
|
||||
value={`€ ${(data.totaleAcconti || 0).toFixed(2)}`}
|
||||
@@ -601,7 +603,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<TextField
|
||||
label="Saldo"
|
||||
label={t("events.detail.fields.balance")}
|
||||
fullWidth
|
||||
size="small"
|
||||
value={`€ ${(data.saldo || (data.costoTotale || 0) - (data.totaleAcconti || 0)).toFixed(2)}`}
|
||||
@@ -616,10 +618,10 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 2 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Stato</InputLabel>
|
||||
<InputLabel>{t("events.detail.fields.status")}</InputLabel>
|
||||
<Select
|
||||
value={data.stato ?? StatoEvento.Scheda}
|
||||
label="Stato"
|
||||
label={t("events.detail.fields.status")}
|
||||
onChange={(e) => {
|
||||
if (!isNew) {
|
||||
cambiaStatoMutation.mutate(e.target.value as StatoEvento);
|
||||
@@ -628,9 +630,9 @@ export default function EventoDetailPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value={StatoEvento.Scheda}>Scheda</MenuItem>
|
||||
<MenuItem value={StatoEvento.Preventivo}>Preventivo</MenuItem>
|
||||
<MenuItem value={StatoEvento.Confermato}>Confermato</MenuItem>
|
||||
<MenuItem value={StatoEvento.Scheda}>{t("events.detail.status.draft")}</MenuItem>
|
||||
<MenuItem value={StatoEvento.Preventivo}>{t("events.detail.status.quote")}</MenuItem>
|
||||
<MenuItem value={StatoEvento.Confermato}>{t("events.detail.status.confirmed")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
@@ -645,13 +647,13 @@ export default function EventoDetailPage() {
|
||||
onChange={(_, v) => setTabValue(v)}
|
||||
sx={{ borderBottom: 1, borderColor: "divider" }}
|
||||
>
|
||||
<Tab label={`Ospiti (${evento?.dettagliOspiti?.length || 0})`} />
|
||||
<Tab label={`${t("events.detail.tabs.guests")} (${evento?.dettagliOspiti?.length || 0})`} />
|
||||
<Tab
|
||||
label={`Lista Prelievo (${evento?.dettagliPrelievo?.length || 0})`}
|
||||
label={`${t("events.detail.tabs.withdrawalList")} (${evento?.dettagliPrelievo?.length || 0})`}
|
||||
/>
|
||||
<Tab label={`Risorse (${evento?.dettagliRisorse?.length || 0})`} />
|
||||
<Tab label="Costi" />
|
||||
<Tab label="Note" />
|
||||
<Tab label={`${t("events.detail.tabs.resources")} (${evento?.dettagliRisorse?.length || 0})`} />
|
||||
<Tab label={t("events.detail.tabs.costs")} />
|
||||
<Tab label={t("events.detail.tabs.notes")} />
|
||||
</Tabs>
|
||||
|
||||
{/* Tab Ospiti */}
|
||||
@@ -660,7 +662,7 @@ export default function EventoDetailPage() {
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
||||
>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Totale ospiti: <strong>{totaleOspiti}</strong>
|
||||
{t("events.detail.guestsTab.total")}: <strong>{totaleOspiti}</strong>
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
@@ -671,7 +673,7 @@ export default function EventoDetailPage() {
|
||||
setDialogOpen("ospite");
|
||||
}}
|
||||
>
|
||||
Aggiungi Tipo Ospite
|
||||
{t("events.detail.guestsTab.add")}
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
@@ -679,13 +681,13 @@ export default function EventoDetailPage() {
|
||||
<TableHead>
|
||||
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
||||
<TableCell>
|
||||
<strong>Tipo Ospite</strong>
|
||||
<strong>{t("events.detail.guestsTab.type")}</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Quantità</strong>
|
||||
<strong>{t("events.detail.guestsTab.quantity")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Note</strong>
|
||||
<strong>{t("events.detail.guestsTab.notes")}</strong>
|
||||
</TableCell>
|
||||
<TableCell width={50}></TableCell>
|
||||
</TableRow>
|
||||
@@ -711,17 +713,16 @@ export default function EventoDetailPage() {
|
||||
))}
|
||||
{(!evento?.dettagliOspiti ||
|
||||
evento.dettagliOspiti.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
Nessun ospite aggiunto. Clicca "Aggiungi Tipo Ospite"
|
||||
per iniziare.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
{t("events.detail.guestsTab.empty")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -733,7 +734,7 @@ export default function EventoDetailPage() {
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
||||
>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Articoli in lista:{" "}
|
||||
{t("events.detail.withdrawalTab.total")}:{" "}
|
||||
<strong>{evento?.dettagliPrelievo?.length || 0}</strong>
|
||||
</Typography>
|
||||
<Button
|
||||
@@ -745,7 +746,7 @@ export default function EventoDetailPage() {
|
||||
setDialogOpen("prelievo");
|
||||
}}
|
||||
>
|
||||
Aggiungi Articolo
|
||||
{t("events.detail.withdrawalTab.add")}
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
@@ -753,22 +754,22 @@ export default function EventoDetailPage() {
|
||||
<TableHead>
|
||||
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
||||
<TableCell>
|
||||
<strong>Codice</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.code")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Articolo</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.article")}</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Qta Richiesta</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.qtyRequested")}</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Qta Calcolata</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.qtyCalculated")}</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Qta Effettiva</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.qtyActual")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Note</strong>
|
||||
<strong>{t("events.detail.withdrawalTab.notes")}</strong>
|
||||
</TableCell>
|
||||
<TableCell width={50}></TableCell>
|
||||
</TableRow>
|
||||
@@ -807,17 +808,16 @@ export default function EventoDetailPage() {
|
||||
))}
|
||||
{(!evento?.dettagliPrelievo ||
|
||||
evento.dettagliPrelievo.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
Nessun articolo in lista. Clicca "Aggiungi Articolo" per
|
||||
iniziare.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={7}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
{t("events.detail.withdrawalTab.empty")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -829,7 +829,7 @@ export default function EventoDetailPage() {
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
||||
>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Risorse assegnate:{" "}
|
||||
{t("events.detail.resourcesTab.total")}:{" "}
|
||||
<strong>{evento?.dettagliRisorse?.length || 0}</strong>
|
||||
</Typography>
|
||||
<Button
|
||||
@@ -841,7 +841,7 @@ export default function EventoDetailPage() {
|
||||
setDialogOpen("risorsa");
|
||||
}}
|
||||
>
|
||||
Aggiungi Risorsa
|
||||
{t("events.detail.resourcesTab.add")}
|
||||
</Button>
|
||||
</Box>
|
||||
<TableContainer>
|
||||
@@ -849,19 +849,19 @@ export default function EventoDetailPage() {
|
||||
<TableHead>
|
||||
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
||||
<TableCell>
|
||||
<strong>Risorsa</strong>
|
||||
<strong>{t("events.detail.resourcesTab.resource")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Ruolo</strong>
|
||||
<strong>{t("common.role")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Ora Inizio</strong>
|
||||
<strong>{t("events.detail.fields.startTime")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Ora Fine</strong>
|
||||
<strong>{t("events.detail.fields.endTime")}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<strong>Note</strong>
|
||||
<strong>{t("events.detail.resourcesTab.notes")}</strong>
|
||||
</TableCell>
|
||||
<TableCell width={50}></TableCell>
|
||||
</TableRow>
|
||||
@@ -891,17 +891,16 @@ export default function EventoDetailPage() {
|
||||
))}
|
||||
{(!evento?.dettagliRisorse ||
|
||||
evento.dettagliRisorse.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
Nessuna risorsa assegnata. Clicca "Aggiungi Risorsa" per
|
||||
iniziare.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
align="center"
|
||||
sx={{ py: 4, color: "text.secondary" }}
|
||||
>
|
||||
{t("events.detail.resourcesTab.empty")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -979,14 +978,14 @@ export default function EventoDetailPage() {
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Aggiungi Tipo Ospite</DialogTitle>
|
||||
<DialogTitle>{t("events.detail.dialogs.addGuest")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo Ospite</InputLabel>
|
||||
<InputLabel>{t("events.detail.guestsTab.type")}</InputLabel>
|
||||
<Select
|
||||
value={dialogData.tipoOspiteId || ""}
|
||||
label="Tipo Ospite"
|
||||
label={t("events.detail.guestsTab.type")}
|
||||
onChange={(e) =>
|
||||
setDialogData({ ...dialogData, tipoOspiteId: e.target.value })
|
||||
}
|
||||
@@ -999,7 +998,7 @@ export default function EventoDetailPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Quantità"
|
||||
label={t("events.detail.guestsTab.quantity")}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={dialogData.quantita || ""}
|
||||
@@ -1011,7 +1010,7 @@ export default function EventoDetailPage() {
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("events.detail.guestsTab.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
@@ -1023,12 +1022,12 @@ export default function EventoDetailPage() {
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(null)}>Annulla</Button>
|
||||
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => addOspiteMutation.mutate(dialogData)}
|
||||
>
|
||||
Aggiungi
|
||||
{t("events.detail.dialogs.add")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -1040,7 +1039,7 @@ export default function EventoDetailPage() {
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Aggiungi Articolo alla Lista</DialogTitle>
|
||||
<DialogTitle>{t("events.detail.dialogs.addArticle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<Autocomplete
|
||||
@@ -1052,11 +1051,11 @@ export default function EventoDetailPage() {
|
||||
setDialogData({ ...dialogData, articoloId: newValue?.id })
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Articolo" fullWidth />
|
||||
<TextField {...params} label={t("events.detail.withdrawalTab.article")} fullWidth />
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
label="Quantità Richiesta"
|
||||
label={t("events.detail.withdrawalTab.qtyRequested")}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={dialogData.qtaRichiesta || ""}
|
||||
@@ -1068,7 +1067,7 @@ export default function EventoDetailPage() {
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("events.detail.withdrawalTab.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
@@ -1080,12 +1079,12 @@ export default function EventoDetailPage() {
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(null)}>Annulla</Button>
|
||||
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => addPrelievoMutation.mutate(dialogData)}
|
||||
>
|
||||
Aggiungi
|
||||
{t("events.detail.dialogs.add")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -1097,7 +1096,7 @@ export default function EventoDetailPage() {
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Aggiungi Risorsa</DialogTitle>
|
||||
<DialogTitle>{t("events.detail.dialogs.addResource")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
||||
<Autocomplete
|
||||
@@ -1109,11 +1108,11 @@ export default function EventoDetailPage() {
|
||||
setDialogData({ ...dialogData, risorsaId: newValue?.id })
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Risorsa" fullWidth />
|
||||
<TextField {...params} label={t("events.detail.resourcesTab.resource")} fullWidth />
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
label="Ruolo"
|
||||
label={t("common.role")}
|
||||
fullWidth
|
||||
value={dialogData.ruolo || ""}
|
||||
onChange={(e) =>
|
||||
@@ -1124,7 +1123,7 @@ export default function EventoDetailPage() {
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TimePicker
|
||||
label="Ora Inizio"
|
||||
label={t("events.detail.fields.startTime")}
|
||||
value={
|
||||
dialogData.oraInizio
|
||||
? dayjs(`2000-01-01T${dialogData.oraInizio}`)
|
||||
@@ -1141,7 +1140,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<TimePicker
|
||||
label="Ora Fine"
|
||||
label={t("events.detail.fields.endTime")}
|
||||
value={
|
||||
dialogData.oraFine
|
||||
? dayjs(`2000-01-01T${dialogData.oraFine}`)
|
||||
@@ -1158,7 +1157,7 @@ export default function EventoDetailPage() {
|
||||
</Grid>
|
||||
</Grid>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t("events.detail.resourcesTab.notes")}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
@@ -1170,12 +1169,12 @@ export default function EventoDetailPage() {
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(null)}>Annulla</Button>
|
||||
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => addRisorsaMutation.mutate(dialogData)}
|
||||
>
|
||||
Aggiungi
|
||||
{t("events.detail.dialogs.add")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -15,11 +15,13 @@ import {
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { locationService } from '../services/lookupService';
|
||||
import { Location } from '../types';
|
||||
|
||||
export default function LocationPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Location>>({ attivo: true });
|
||||
@@ -71,15 +73,15 @@ export default function LocationPage() {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nome', headerName: 'Nome', flex: 1, minWidth: 200 },
|
||||
{ field: 'citta', headerName: 'Città', width: 150 },
|
||||
{ field: 'provincia', headerName: 'Prov.', width: 80 },
|
||||
{ field: 'distanzaKm', headerName: 'Distanza (km)', width: 120, type: 'number' },
|
||||
{ field: 'referente', headerName: 'Referente', width: 150 },
|
||||
{ field: 'telefono', headerName: 'Telefono', width: 130 },
|
||||
{ field: 'nome', headerName: t('location.name'), flex: 1, minWidth: 200 },
|
||||
{ field: 'citta', headerName: t('location.city'), width: 150 },
|
||||
{ field: 'provincia', headerName: t('location.province'), width: 80 },
|
||||
{ field: 'distanzaKm', headerName: t('location.distance'), width: 120, type: 'number' },
|
||||
{ field: 'referente', headerName: t('location.contact'), width: 150 },
|
||||
{ field: 'telefono', headerName: t('location.phone'), width: 130 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: 'Azioni',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -91,7 +93,7 @@ export default function LocationPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm('Eliminare questa location?')) {
|
||||
if (confirm(t('location.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -106,9 +108,9 @@ export default function LocationPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">Location</Typography>
|
||||
<Typography variant="h4">{t('location.title')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
Nuova Location
|
||||
{t('location.newLocation')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -126,12 +128,12 @@ export default function LocationPage() {
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? 'Modifica Location' : 'Nuova Location'}</DialogTitle>
|
||||
<DialogTitle>{editingId ? t('location.editLocation') : t('location.newLocation')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Nome"
|
||||
label={t('location.name')}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.nome || ''}
|
||||
@@ -140,7 +142,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<TextField
|
||||
label="Indirizzo"
|
||||
label={t('location.address')}
|
||||
fullWidth
|
||||
value={formData.indirizzo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, indirizzo: e.target.value })}
|
||||
@@ -148,7 +150,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="CAP"
|
||||
label={t('location.zip')}
|
||||
fullWidth
|
||||
value={formData.cap || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cap: e.target.value })}
|
||||
@@ -156,7 +158,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Città"
|
||||
label={t('location.city')}
|
||||
fullWidth
|
||||
value={formData.citta || ''}
|
||||
onChange={(e) => setFormData({ ...formData, citta: e.target.value })}
|
||||
@@ -164,7 +166,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Provincia"
|
||||
label={t('location.province')}
|
||||
fullWidth
|
||||
value={formData.provincia || ''}
|
||||
onChange={(e) => setFormData({ ...formData, provincia: e.target.value })}
|
||||
@@ -172,7 +174,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Distanza (km)"
|
||||
label={t('location.distance')}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.distanzaKm || ''}
|
||||
@@ -181,7 +183,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Telefono"
|
||||
label={t('location.phone')}
|
||||
fullWidth
|
||||
value={formData.telefono || ''}
|
||||
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
|
||||
@@ -189,7 +191,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Email"
|
||||
label={t('location.email')}
|
||||
fullWidth
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
@@ -198,7 +200,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Referente"
|
||||
label={t('location.contact')}
|
||||
fullWidth
|
||||
value={formData.referente || ''}
|
||||
onChange={(e) => setFormData({ ...formData, referente: e.target.value })}
|
||||
@@ -206,7 +208,7 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t('common.notes')}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
@@ -217,9 +219,9 @@ export default function LocationPage() {
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? 'Salva' : 'Crea'}
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
CalendarToday as AnnualIcon,
|
||||
Warning as WarningIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModule, useModules } from "../contexts/ModuleContext";
|
||||
import {
|
||||
SubscriptionType,
|
||||
@@ -40,6 +41,7 @@ export default function ModulePurchasePage() {
|
||||
const location = useLocation();
|
||||
const module = useModule(code || "");
|
||||
const { enableModule, isModuleEnabled } = useModules();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [subscriptionType, setSubscriptionType] = useState<SubscriptionType>(
|
||||
SubscriptionType.Annual,
|
||||
@@ -72,17 +74,17 @@ export default function ModulePurchasePage() {
|
||||
return (
|
||||
<Box sx={{ p: 3, textAlign: "center" }}>
|
||||
<Typography variant="h5" color="error" gutterBottom>
|
||||
Modulo non trovato
|
||||
{t("modules.admin.moduleNotFound")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" paragraph>
|
||||
Il modulo richiesto non esiste.
|
||||
{t("modules.admin.moduleNotFoundText")}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<BackIcon />}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
Torna alla Home
|
||||
{t("modules.admin.backToHome")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
@@ -95,7 +97,9 @@ export default function ModulePurchasePage() {
|
||||
: module.basePrice;
|
||||
|
||||
const priceLabel =
|
||||
subscriptionType === SubscriptionType.Monthly ? "/mese" : "/anno";
|
||||
subscriptionType === SubscriptionType.Monthly
|
||||
? t("modules.admin.perMonth")
|
||||
: t("modules.admin.perYear");
|
||||
|
||||
// Calcola risparmio annuale
|
||||
const annualSavings = module.monthlyPrice * 12 - module.basePrice;
|
||||
@@ -117,13 +121,13 @@ export default function ModulePurchasePage() {
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
Indietro
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Attiva Modulo
|
||||
{t("modules.admin.purchaseTitle")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Scegli il piano di abbonamento per il modulo {module.name}
|
||||
{t("modules.admin.purchaseSubtitle", { name: module.name })}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -131,7 +135,7 @@ export default function ModulePurchasePage() {
|
||||
{missingDependencies.length > 0 && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Questo modulo richiede i seguenti moduli che non sono attivi:
|
||||
{t("modules.admin.missingDependencies")}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||
{missingDependencies.map((dep) => (
|
||||
@@ -165,7 +169,7 @@ export default function ModulePurchasePage() {
|
||||
{/* Selezione tipo abbonamento */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight="medium">
|
||||
Tipo di abbonamento
|
||||
{t("modules.admin.subscriptionType")}
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={subscriptionType}
|
||||
@@ -178,7 +182,7 @@ export default function ModulePurchasePage() {
|
||||
<Box sx={{ py: 1, textAlign: "center" }}>
|
||||
<MonthlyIcon sx={{ mb: 0.5 }} />
|
||||
<Typography variant="body2" display="block">
|
||||
Mensile
|
||||
{t("modules.admin.monthly")}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.monthlyPrice)}
|
||||
@@ -187,7 +191,7 @@ export default function ModulePurchasePage() {
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
/mese
|
||||
{t("modules.admin.perMonth")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -196,7 +200,7 @@ export default function ModulePurchasePage() {
|
||||
<Box sx={{ py: 1, textAlign: "center" }}>
|
||||
<AnnualIcon sx={{ mb: 0.5 }} />
|
||||
<Typography variant="body2" display="block">
|
||||
Annuale
|
||||
{t("modules.admin.annual")}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(module.basePrice)}
|
||||
@@ -205,12 +209,14 @@ export default function ModulePurchasePage() {
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
/anno
|
||||
{t("modules.admin.perYear")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
{savingsPercent > 0 && (
|
||||
<Chip
|
||||
label={`Risparmi ${savingsPercent}%`}
|
||||
label={t("modules.admin.savings", {
|
||||
percent: savingsPercent,
|
||||
})}
|
||||
size="small"
|
||||
color="success"
|
||||
sx={{ mt: 0.5 }}
|
||||
@@ -231,7 +237,7 @@ export default function ModulePurchasePage() {
|
||||
{autoRenew ? <CheckIcon /> : null}
|
||||
</ToggleButton>
|
||||
<Typography variant="body2">
|
||||
Rinnovo automatico alla scadenza
|
||||
{t("modules.admin.autoRenewLabel")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -244,26 +250,26 @@ export default function ModulePurchasePage() {
|
||||
sx={{ p: 2, mb: 3, bgcolor: "action.hover" }}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Riepilogo ordine
|
||||
{t("modules.admin.orderSummary")}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography>Modulo {module.name}</Typography>
|
||||
<Typography>{t("modules.admin.module")} {module.name}</Typography>
|
||||
<Typography>{formatPrice(price)}</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ display: "flex", justifyContent: "space-between", mb: 1 }}
|
||||
>
|
||||
<Typography color="text.secondary">
|
||||
Abbonamento{" "}
|
||||
{t("modules.admin.subscription")}{" "}
|
||||
{getSubscriptionTypeName(subscriptionType).toLowerCase()}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">{priceLabel}</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography variant="h6">Totale</Typography>
|
||||
<Typography variant="h6">{t("modules.admin.total")}</Typography>
|
||||
<Typography variant="h6" color="primary">
|
||||
{formatPrice(price)}
|
||||
{priceLabel}
|
||||
@@ -275,7 +281,7 @@ export default function ModulePurchasePage() {
|
||||
{enableMutation.isError && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{(enableMutation.error as Error)?.message ||
|
||||
"Errore durante l'attivazione del modulo"}
|
||||
t("modules.admin.activationError")}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -297,8 +303,8 @@ export default function ModulePurchasePage() {
|
||||
}
|
||||
>
|
||||
{enableMutation.isPending
|
||||
? "Attivazione in corso..."
|
||||
: "Attiva Modulo"}
|
||||
? t("modules.admin.activating")
|
||||
: t("modules.admin.activateModule")}
|
||||
</Button>
|
||||
|
||||
{/* Note */}
|
||||
@@ -309,8 +315,7 @@ export default function ModulePurchasePage() {
|
||||
textAlign="center"
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Potrai disattivare il modulo in qualsiasi momento dalle
|
||||
impostazioni. I dati inseriti rimarranno disponibili.
|
||||
{t("modules.admin.purchaseNote")}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -319,10 +324,10 @@ export default function ModulePurchasePage() {
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Funzionalità incluse
|
||||
{t("modules.admin.includedFeatures")}
|
||||
</Typography>
|
||||
<List dense>
|
||||
{getModuleFeatures(module.code).map((feature, index) => (
|
||||
{getModuleFeatures(module.code, t).map((feature, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<CheckIcon color="success" fontSize="small" />
|
||||
@@ -338,49 +343,10 @@ export default function ModulePurchasePage() {
|
||||
}
|
||||
|
||||
// Helper per ottenere le funzionalità di un modulo
|
||||
function getModuleFeatures(code: string): string[] {
|
||||
const features: Record<string, string[]> = {
|
||||
warehouse: [
|
||||
"Gestione anagrafica articoli",
|
||||
"Movimenti di magazzino (carico/scarico)",
|
||||
"Giacenze in tempo reale",
|
||||
"Valorizzazione scorte (FIFO, LIFO, medio ponderato)",
|
||||
"Inventario e rettifiche",
|
||||
"Report giacenze e movimenti",
|
||||
],
|
||||
purchases: [
|
||||
"Gestione ordini a fornitore",
|
||||
"DDT di entrata",
|
||||
"Fatture passive",
|
||||
"Scadenziario pagamenti",
|
||||
"Analisi acquisti per fornitore/articolo",
|
||||
"Storico prezzi di acquisto",
|
||||
],
|
||||
sales: [
|
||||
"Gestione ordini cliente",
|
||||
"DDT di uscita",
|
||||
"Fatturazione elettronica",
|
||||
"Scadenziario incassi",
|
||||
"Analisi vendite per cliente/articolo",
|
||||
"Listini prezzi",
|
||||
],
|
||||
production: [
|
||||
"Distinte base multilivello",
|
||||
"Cicli di lavoro",
|
||||
"Ordini di produzione",
|
||||
"Pianificazione MRP",
|
||||
"Avanzamento produzione",
|
||||
"Costi di produzione",
|
||||
],
|
||||
quality: [
|
||||
"Piani di controllo",
|
||||
"Registrazione controlli",
|
||||
"Gestione non conformità",
|
||||
"Azioni correttive/preventive",
|
||||
"Certificazioni e audit",
|
||||
"Statistiche qualità",
|
||||
],
|
||||
};
|
||||
|
||||
return features[code] || ["Funzionalità complete del modulo"];
|
||||
function getModuleFeatures(code: string, t: (key: string) => string): string[] {
|
||||
const featureKeys = [0, 1, 2, 3, 4, 5];
|
||||
if (["warehouse", "purchases", "sales", "production", "quality"].includes(code)) {
|
||||
return featureKeys.map((i) => t(`modules.features.${code}.${i}`));
|
||||
}
|
||||
return [t("modules.features.default")];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Autorenew as RenewIcon,
|
||||
} from "@mui/icons-material";
|
||||
import * as Icons from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useModules } from "../contexts/ModuleContext";
|
||||
import { moduleService } from "../services/moduleService";
|
||||
import type { ModuleDto } from "../types/module";
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
|
||||
export default function ModulesAdminPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { modules, isLoading, refreshModules } = useModules();
|
||||
const [selectedModule, setSelectedModule] = useState<ModuleDto | null>(null);
|
||||
const [confirmDisable, setConfirmDisable] = useState<string | null>(null);
|
||||
@@ -108,10 +110,10 @@ export default function ModulesAdminPage() {
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Gestione Moduli
|
||||
{t("modules.admin.title")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Configura i moduli attivi e gestisci le subscription
|
||||
{t("modules.admin.subtitle")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
@@ -121,14 +123,14 @@ export default function ModulesAdminPage() {
|
||||
onClick={() => checkExpiredMutation.mutate()}
|
||||
disabled={checkExpiredMutation.isPending}
|
||||
>
|
||||
Controlla Scadenze
|
||||
{t("modules.admin.checkExpired")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => refreshModules()}
|
||||
>
|
||||
Aggiorna
|
||||
{t("modules.admin.refresh")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -137,8 +139,9 @@ export default function ModulesAdminPage() {
|
||||
{expiringModules.length > 0 && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }} icon={<WarningIcon />}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{expiringModules.length} modulo/i in scadenza nei prossimi 30
|
||||
giorni:
|
||||
{t("modules.admin.expiringWarning", {
|
||||
count: expiringModules.length,
|
||||
})}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
|
||||
{expiringModules.map((m) => (
|
||||
@@ -170,6 +173,7 @@ export default function ModulesAdminPage() {
|
||||
onRenew={() => renewMutation.mutate(module.code)}
|
||||
isRenewing={renewMutation.isPending}
|
||||
getIcon={getModuleIcon}
|
||||
t={t}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
@@ -198,7 +202,7 @@ export default function ModulesAdminPage() {
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Prezzo annuale
|
||||
{t("modules.admin.annualPrice")}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{formatPrice(selectedModule.basePrice)}
|
||||
@@ -206,7 +210,7 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Prezzo mensile
|
||||
{t("modules.admin.monthlyPrice")}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{formatPrice(selectedModule.monthlyPrice)}
|
||||
@@ -217,7 +221,7 @@ export default function ModulesAdminPage() {
|
||||
{selectedModule.dependencies.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Dipendenze
|
||||
{t("modules.admin.dependencies")}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 0.5, mt: 0.5 }}>
|
||||
{selectedModule.dependencies.map((dep) => (
|
||||
@@ -231,12 +235,12 @@ export default function ModulesAdminPage() {
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Dettagli Subscription
|
||||
{t("modules.admin.subscriptionDetails")}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Tipo
|
||||
{t("modules.admin.type")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{selectedModule.subscription.subscriptionTypeName}
|
||||
@@ -244,7 +248,7 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Stato
|
||||
{t("modules.admin.status")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Chip
|
||||
@@ -260,7 +264,7 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Data inizio
|
||||
{t("modules.admin.startDate")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{formatDate(selectedModule.subscription.startDate)}
|
||||
@@ -268,7 +272,7 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Data scadenza
|
||||
{t("modules.admin.endDate")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{formatDate(selectedModule.subscription.endDate)}
|
||||
@@ -276,7 +280,7 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Giorni rimanenti
|
||||
{t("modules.admin.daysRemaining")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{getDaysRemainingText(
|
||||
@@ -286,10 +290,12 @@ export default function ModulesAdminPage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Rinnovo automatico
|
||||
{t("modules.admin.autoRenew")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{selectedModule.subscription.autoRenew ? "Sì" : "No"}
|
||||
{selectedModule.subscription.autoRenew
|
||||
? t("modules.admin.yes")
|
||||
: t("modules.admin.no")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -297,7 +303,9 @@ export default function ModulesAdminPage() {
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setSelectedModule(null)}>Chiudi</Button>
|
||||
<Button onClick={() => setSelectedModule(null)}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
@@ -310,28 +318,29 @@ export default function ModulesAdminPage() {
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Conferma disattivazione</DialogTitle>
|
||||
<DialogTitle>{t("modules.admin.disableConfirmTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler disattivare il modulo{" "}
|
||||
{t("modules.admin.disableConfirmText")}{" "}
|
||||
<strong>
|
||||
{modules.find((m) => m.code === confirmDisable)?.name}
|
||||
</strong>
|
||||
?
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mt: 1 }}>
|
||||
I dati inseriti rimarranno nel sistema ma non saranno più
|
||||
accessibili fino alla riattivazione.
|
||||
{t("modules.admin.disableConfirmSubtext")}
|
||||
</Typography>
|
||||
{disableMutation.isError && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{(disableMutation.error as Error)?.message ||
|
||||
"Errore durante la disattivazione"}
|
||||
t("common.error")}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmDisable(null)}>Annulla</Button>
|
||||
<Button onClick={() => setConfirmDisable(null)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
@@ -345,7 +354,7 @@ export default function ModulesAdminPage() {
|
||||
) : null
|
||||
}
|
||||
>
|
||||
Disattiva
|
||||
{t("modules.admin.disable")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -361,6 +370,7 @@ interface ModuleCardProps {
|
||||
onRenew: () => void;
|
||||
isRenewing: boolean;
|
||||
getIcon: (iconName?: string) => React.ReactNode;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
function ModuleCard({
|
||||
@@ -370,6 +380,7 @@ function ModuleCard({
|
||||
onRenew,
|
||||
isRenewing,
|
||||
getIcon,
|
||||
t,
|
||||
}: ModuleCardProps) {
|
||||
const statusColor = getSubscriptionStatusColor(module.subscription);
|
||||
const statusText = getSubscriptionStatusText(module.subscription);
|
||||
@@ -409,7 +420,7 @@ function ModuleCard({
|
||||
</Box>
|
||||
<Box>
|
||||
{module.isCore ? (
|
||||
<Chip label="Core" size="small" color="info" />
|
||||
<Chip label={t("modules.admin.core")} size="small" color="info" />
|
||||
) : (
|
||||
<Chip label={statusText} size="small" color={statusColor} />
|
||||
)}
|
||||
@@ -435,7 +446,7 @@ function ModuleCard({
|
||||
{module.subscription && module.isEnabled && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Scadenza: {formatDate(module.subscription.endDate)}
|
||||
{t("modules.admin.endDate")}: {formatDate(module.subscription.endDate)}
|
||||
{module.subscription.daysRemaining !== undefined &&
|
||||
module.subscription.daysRemaining <= 30 && (
|
||||
<Chip
|
||||
@@ -475,7 +486,7 @@ function ModuleCard({
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Tooltip title="Dettagli">
|
||||
<Tooltip title={t("modules.admin.details")}>
|
||||
<IconButton size="small" onClick={onInfo}>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
@@ -483,7 +494,7 @@ function ModuleCard({
|
||||
{module.isEnabled &&
|
||||
!module.isCore &&
|
||||
module.subscription?.isExpiringSoon && (
|
||||
<Tooltip title="Rinnova">
|
||||
<Tooltip title={t("modules.admin.renew")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
@@ -505,7 +516,7 @@ function ModuleCard({
|
||||
color={module.isEnabled ? "success" : "default"}
|
||||
/>
|
||||
}
|
||||
label={module.isEnabled ? "Attivo" : "Disattivo"}
|
||||
label={module.isEnabled ? t("modules.admin.active") : t("modules.admin.inactive")}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
Close as CloseIcon,
|
||||
Layers as LayersIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EditorCanvas, {
|
||||
type ContextMenuEvent,
|
||||
type EditorCanvasRef,
|
||||
@@ -101,6 +102,7 @@ export default function ReportEditorPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const isNew = !id;
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Responsive breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px
|
||||
@@ -118,7 +120,7 @@ export default function ReportEditorPage() {
|
||||
descrizione: string;
|
||||
categoria: string;
|
||||
}>({
|
||||
nome: "Nuovo Template",
|
||||
nome: t("reports.editor.newTemplate"),
|
||||
descrizione: "",
|
||||
categoria: "Generale",
|
||||
});
|
||||
@@ -370,7 +372,7 @@ export default function ReportEditorPage() {
|
||||
// Show notification
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Template aggiornato da un altro utente",
|
||||
message: t("reports.editor.templateUpdatedByOther"),
|
||||
severity: "success",
|
||||
});
|
||||
|
||||
@@ -596,7 +598,7 @@ export default function ReportEditorPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["report-templates"] });
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Template salvato con successo",
|
||||
message: t("reports.editor.saveSuccess"),
|
||||
severity: "success",
|
||||
});
|
||||
setSaveDialog(false);
|
||||
@@ -636,7 +638,7 @@ export default function ReportEditorPage() {
|
||||
onError: (error) => {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: `Errore nel salvataggio: ${error}`,
|
||||
message: t("reports.editor.saveError", { error }),
|
||||
severity: "error",
|
||||
});
|
||||
},
|
||||
@@ -693,7 +695,7 @@ export default function ReportEditorPage() {
|
||||
const newPageId = `page-${uuidv4().slice(0, 8)}`;
|
||||
const newPage: AprtPage = {
|
||||
id: newPageId,
|
||||
name: `Pagina ${template.pages.length + 1}`,
|
||||
name: t("reports.editor.pageName", { number: template.pages.length + 1 }),
|
||||
};
|
||||
|
||||
historyActions.set((prev) => ({
|
||||
@@ -721,7 +723,7 @@ export default function ReportEditorPage() {
|
||||
const newPage: AprtPage = {
|
||||
...sourcePage,
|
||||
id: newPageId,
|
||||
name: `${sourcePage.name} (copia)`,
|
||||
name: t("reports.editor.copyOf", { name: sourcePage.name }),
|
||||
};
|
||||
|
||||
// Duplicate elements from source page
|
||||
@@ -871,7 +873,7 @@ export default function ReportEditorPage() {
|
||||
style: { ...defaultStyle },
|
||||
content:
|
||||
type === "text"
|
||||
? { type: "static", value: "Nuovo testo" }
|
||||
? { type: "static", value: t("reports.editor.newText") }
|
||||
: undefined,
|
||||
visible: true,
|
||||
locked: false,
|
||||
@@ -879,19 +881,19 @@ export default function ReportEditorPage() {
|
||||
columns:
|
||||
type === "table"
|
||||
? [
|
||||
{
|
||||
field: "campo1",
|
||||
header: "Colonna 1",
|
||||
width: 50,
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
field: "campo2",
|
||||
header: "Colonna 2",
|
||||
width: 50,
|
||||
align: "left",
|
||||
},
|
||||
]
|
||||
{
|
||||
field: "campo1",
|
||||
header: t("reports.editor.column", { number: 1 }),
|
||||
width: 50,
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
field: "campo2",
|
||||
header: t("reports.editor.column", { number: 2 }),
|
||||
width: 50,
|
||||
align: "left",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -1048,7 +1050,7 @@ export default function ReportEditorPage() {
|
||||
const copy: AprtElement = {
|
||||
...selectedElement,
|
||||
id: uuidv4(),
|
||||
name: `${selectedElement.name}_copia`,
|
||||
name: `${selectedElement.name}${t("reports.editor.copySuffix")}`,
|
||||
position: {
|
||||
...selectedElement.position,
|
||||
x: selectedElement.position.x + 10,
|
||||
@@ -1139,7 +1141,7 @@ export default function ReportEditorPage() {
|
||||
setClipboard({ ...selectedElement });
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Elemento copiato",
|
||||
message: t("reports.editor.elementCopied"),
|
||||
severity: "success",
|
||||
});
|
||||
}, [selectedElement]);
|
||||
@@ -1150,7 +1152,7 @@ export default function ReportEditorPage() {
|
||||
const pastedElement: AprtElement = {
|
||||
...clipboard,
|
||||
id: uuidv4(),
|
||||
name: `${clipboard.name}_incollato`,
|
||||
name: `${clipboard.name}${t("reports.editor.pastedSuffix")}`,
|
||||
position: {
|
||||
...clipboard.position,
|
||||
x: clipboard.position.x + 10,
|
||||
@@ -1399,7 +1401,7 @@ export default function ReportEditorPage() {
|
||||
const handleGroup = useCallback(() => {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Raggruppamento non ancora implementato",
|
||||
message: t("reports.editor.groupingNotImplemented"),
|
||||
severity: "error",
|
||||
});
|
||||
}, []);
|
||||
@@ -1407,7 +1409,7 @@ export default function ReportEditorPage() {
|
||||
const handleUngroup = useCallback(() => {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Separazione non ancora implementata",
|
||||
message: t("reports.editor.ungroupingNotImplemented"),
|
||||
severity: "error",
|
||||
});
|
||||
}, []);
|
||||
@@ -1445,7 +1447,7 @@ export default function ReportEditorPage() {
|
||||
// For now, just select the element
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Fai doppio click sul testo per modificarlo",
|
||||
message: t("reports.editor.doubleClickToEdit"),
|
||||
severity: "success",
|
||||
});
|
||||
}, []);
|
||||
@@ -1462,7 +1464,7 @@ export default function ReportEditorPage() {
|
||||
// This would need to calculate text dimensions
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Adatta al contenuto non ancora implementato",
|
||||
message: t("reports.editor.fitToContentNotImplemented"),
|
||||
severity: "error",
|
||||
});
|
||||
}, []);
|
||||
@@ -1483,7 +1485,7 @@ export default function ReportEditorPage() {
|
||||
if (selectedDatasets.length === 0) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Seleziona almeno un dataset per l'anteprima",
|
||||
message: t("reports.editor.selectDatasetForPreview"),
|
||||
severity: "error",
|
||||
});
|
||||
return;
|
||||
@@ -1501,7 +1503,7 @@ export default function ReportEditorPage() {
|
||||
if (isNew) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Salva il template prima di visualizzare l'anteprima",
|
||||
message: t("reports.editor.saveBeforePreview"),
|
||||
severity: "error",
|
||||
});
|
||||
setPreviewDialog(false);
|
||||
@@ -1518,7 +1520,7 @@ export default function ReportEditorPage() {
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: `Errore nella generazione dell'anteprima: ${error}`,
|
||||
message: t("reports.editor.previewError", { error }),
|
||||
severity: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -1847,11 +1849,11 @@ export default function ReportEditorPage() {
|
||||
const getMobilePanelTitle = () => {
|
||||
switch (mobilePanel) {
|
||||
case "pages":
|
||||
return "Pagine";
|
||||
return t("reports.editor.panels.pages");
|
||||
case "data":
|
||||
return "Campi Dati";
|
||||
return t("reports.editor.panels.data");
|
||||
case "properties":
|
||||
return "Proprietà";
|
||||
return t("reports.editor.panels.properties");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -1902,7 +1904,7 @@ export default function ReportEditorPage() {
|
||||
isSaving={saveMutation.isPending}
|
||||
currentPageIndex={currentPageIndex}
|
||||
totalPages={template.pages.length}
|
||||
currentPageName={currentPage?.name || "Pagina 1"}
|
||||
currentPageName={currentPage?.name || t("reports.editor.defaultPageName")}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
@@ -1932,7 +1934,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
title={t("reports.editor.panels.pages")}
|
||||
icon={<LayersIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
@@ -1955,7 +1957,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
title={t("reports.editor.panels.data")}
|
||||
icon={<DataIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
@@ -1982,7 +1984,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
title={t("reports.editor.panels.properties")}
|
||||
icon={<SettingsIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
@@ -2073,7 +2075,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
title={t("reports.editor.panels.pages")}
|
||||
icon={<LayersIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
@@ -2096,7 +2098,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
title={t("reports.editor.panels.data")}
|
||||
icon={<DataIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
@@ -2123,7 +2125,7 @@ export default function ReportEditorPage() {
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
title={t("reports.editor.panels.properties")}
|
||||
icon={<SettingsIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
@@ -2169,17 +2171,17 @@ export default function ReportEditorPage() {
|
||||
showLabels
|
||||
>
|
||||
<BottomNavigationAction
|
||||
label="Pagine"
|
||||
label={t("reports.editor.panels.pages")}
|
||||
value="pages"
|
||||
icon={<PageIcon />}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="Dati"
|
||||
label={t("reports.editor.panels.data")}
|
||||
value="data"
|
||||
icon={<DataIcon />}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="Proprietà"
|
||||
label={t("reports.editor.panels.properties")}
|
||||
value="properties"
|
||||
icon={<SettingsIcon />}
|
||||
/>
|
||||
@@ -2192,7 +2194,7 @@ export default function ReportEditorPage() {
|
||||
anchor="bottom"
|
||||
open={isMobile && mobilePanel !== null}
|
||||
onClose={() => setMobilePanel(null)}
|
||||
onOpen={() => {}}
|
||||
onOpen={() => { }}
|
||||
disableSwipeToOpen
|
||||
PaperProps={{
|
||||
sx: {
|
||||
@@ -2241,11 +2243,11 @@ export default function ReportEditorPage() {
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Salva Template</DialogTitle>
|
||||
<DialogTitle>{t("reports.editor.saveDialog.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box display="flex" flexDirection="column" gap={2} mt={1}>
|
||||
<TextField
|
||||
label="Nome"
|
||||
label={t("reports.editor.saveDialog.name")}
|
||||
value={templateInfo.nome}
|
||||
onChange={(e) =>
|
||||
setTemplateInfo((prev) => ({ ...prev, nome: e.target.value }))
|
||||
@@ -2254,7 +2256,7 @@ export default function ReportEditorPage() {
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
label={t("reports.editor.saveDialog.description")}
|
||||
value={templateInfo.descrizione}
|
||||
onChange={(e) =>
|
||||
setTemplateInfo((prev) => ({
|
||||
@@ -2267,10 +2269,10 @@ export default function ReportEditorPage() {
|
||||
rows={2}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<InputLabel>{t("reports.editor.saveDialog.category")}</InputLabel>
|
||||
<Select
|
||||
value={templateInfo.categoria}
|
||||
label="Categoria"
|
||||
label={t("reports.editor.saveDialog.category")}
|
||||
onChange={(e) =>
|
||||
setTemplateInfo((prev) => ({
|
||||
...prev,
|
||||
@@ -2278,17 +2280,17 @@ export default function ReportEditorPage() {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<MenuItem value="Generale">Generale</MenuItem>
|
||||
<MenuItem value="Evento">Evento</MenuItem>
|
||||
<MenuItem value="Cliente">Cliente</MenuItem>
|
||||
<MenuItem value="Articoli">Articoli</MenuItem>
|
||||
<MenuItem value="Generale">{t("reports.categories.Generale")}</MenuItem>
|
||||
<MenuItem value="Evento">{t("reports.categories.Evento")}</MenuItem>
|
||||
<MenuItem value="Cliente">{t("reports.categories.Cliente")}</MenuItem>
|
||||
<MenuItem value="Articoli">{t("reports.categories.Articoli")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: { xs: 2, sm: 1 } }}>
|
||||
<Button onClick={() => setSaveDialog(false)} fullWidth={isMobile}>
|
||||
Annulla
|
||||
{t("reports.editor.saveDialog.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -2298,7 +2300,7 @@ export default function ReportEditorPage() {
|
||||
disabled={!templateInfo.nome || saveMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{saveMutation.isPending ? "Salvataggio..." : "Salva"}
|
||||
{saveMutation.isPending ? t("reports.editor.saveDialog.saving") : t("reports.editor.saveDialog.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Upload as UploadIcon,
|
||||
Description as DescriptionIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { reportTemplateService, downloadBlob } from "../services/reportService";
|
||||
import type { ReportTemplateDto } from "../types/report";
|
||||
|
||||
@@ -44,6 +45,7 @@ export default function ReportTemplatesPage() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@@ -161,7 +163,7 @@ export default function ReportTemplatesPage() {
|
||||
}}
|
||||
>
|
||||
<Typography variant={isMobile ? "h5" : "h4"} sx={{ fontWeight: 600 }}>
|
||||
Template Report
|
||||
{t("reports.title")}
|
||||
</Typography>
|
||||
|
||||
{/* Desktop/Tablet buttons */}
|
||||
@@ -172,14 +174,14 @@ export default function ReportTemplatesPage() {
|
||||
startIcon={<UploadIcon />}
|
||||
onClick={() => setImportDialog(true)}
|
||||
>
|
||||
Importa
|
||||
{t("reports.import")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => navigate("/report-editor")}
|
||||
>
|
||||
Nuovo Template
|
||||
{t("reports.newTemplate")}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -196,16 +198,16 @@ export default function ReportTemplatesPage() {
|
||||
}}
|
||||
>
|
||||
<FormControl size="small" sx={{ minWidth: { xs: "100%", sm: 200 } }}>
|
||||
<InputLabel>Filtra per categoria</InputLabel>
|
||||
<InputLabel>{t("reports.filterCategory")}</InputLabel>
|
||||
<Select
|
||||
value={filterCategoria}
|
||||
label="Filtra per categoria"
|
||||
label={t("reports.filterCategory")}
|
||||
onChange={(e) => setFilterCategoria(e.target.value)}
|
||||
>
|
||||
<MenuItem value="">Tutte</MenuItem>
|
||||
<MenuItem value="">{t("reports.all")}</MenuItem>
|
||||
{categories.map((cat) => (
|
||||
<MenuItem key={cat} value={cat}>
|
||||
{cat}
|
||||
{t(`reports.categories.${cat}`) || cat}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -219,7 +221,7 @@ export default function ReportTemplatesPage() {
|
||||
onClick={() => setImportDialog(true)}
|
||||
fullWidth
|
||||
>
|
||||
Importa Template
|
||||
{t("reports.importTemplate")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -242,10 +244,10 @@ export default function ReportTemplatesPage() {
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
Nessun template trovato
|
||||
{t("reports.noTemplates")}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" mb={3}>
|
||||
Crea il tuo primo template di report o importane uno esistente
|
||||
{t("reports.createFirstTemplate")}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
@@ -258,7 +260,7 @@ export default function ReportTemplatesPage() {
|
||||
onClick={() => setImportDialog(true)}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Importa Template
|
||||
{t("reports.importTemplate")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -266,7 +268,7 @@ export default function ReportTemplatesPage() {
|
||||
onClick={() => navigate("/report-editor")}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Crea Template
|
||||
{t("reports.createTemplate")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
@@ -342,7 +344,7 @@ export default function ReportTemplatesPage() {
|
||||
{template.nome}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={template.categoria}
|
||||
label={t(`reports.categories.${template.categoria}`) || template.categoria}
|
||||
size="small"
|
||||
color={getCategoriaColor(template.categoria)}
|
||||
sx={{ flexShrink: 0 }}
|
||||
@@ -366,8 +368,8 @@ export default function ReportTemplatesPage() {
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{template.pageSize} -{" "}
|
||||
{template.orientation === "portrait"
|
||||
? "Verticale"
|
||||
: "Orizzontale"}
|
||||
? t("reports.vertical")
|
||||
: t("reports.horizontal")}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
@@ -382,7 +384,7 @@ export default function ReportTemplatesPage() {
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Tooltip title="Modifica">
|
||||
<Tooltip title={t("reports.edit")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
@@ -392,7 +394,7 @@ export default function ReportTemplatesPage() {
|
||||
<EditIcon fontSize={isMobile ? "small" : "medium"} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Duplica">
|
||||
<Tooltip title={t("reports.duplicate")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => cloneMutation.mutate(template.id)}
|
||||
@@ -400,7 +402,7 @@ export default function ReportTemplatesPage() {
|
||||
<CopyIcon fontSize={isMobile ? "small" : "medium"} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Esporta">
|
||||
<Tooltip title={t("reports.export")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleExport(template)}
|
||||
@@ -411,7 +413,7 @@ export default function ReportTemplatesPage() {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Tooltip title="Elimina">
|
||||
<Tooltip title={t("reports.delete")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
@@ -432,7 +434,7 @@ export default function ReportTemplatesPage() {
|
||||
<Zoom in>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="Nuovo template"
|
||||
aria-label={t("reports.newTemplate")}
|
||||
onClick={() => navigate("/report-editor")}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
@@ -454,14 +456,13 @@ export default function ReportTemplatesPage() {
|
||||
maxWidth="xs"
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Conferma Eliminazione</DialogTitle>
|
||||
<DialogTitle>{t("reports.confirmDelete")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler eliminare il template "
|
||||
{deleteDialog.template?.nome}"?
|
||||
{t("reports.deleteConfirmText", { name: deleteDialog.template?.nome })}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Questa azione non può essere annullata.
|
||||
{t("reports.irreversibleAction")}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: { xs: 2, sm: 1 } }}>
|
||||
@@ -469,7 +470,7 @@ export default function ReportTemplatesPage() {
|
||||
onClick={() => setDeleteDialog({ open: false, template: null })}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Annulla
|
||||
{t("reports.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
@@ -481,7 +482,7 @@ export default function ReportTemplatesPage() {
|
||||
disabled={deleteMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{deleteMutation.isPending ? "Eliminazione..." : "Elimina"}
|
||||
{deleteMutation.isPending ? t("reports.deleting") : t("reports.delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -497,13 +498,13 @@ export default function ReportTemplatesPage() {
|
||||
maxWidth="xs"
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Importa Template</DialogTitle>
|
||||
<DialogTitle>{t("reports.importTitle")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary" mb={2}>
|
||||
Seleziona un file .aprt da importare
|
||||
{t("reports.importText")}
|
||||
</Typography>
|
||||
<Button variant="outlined" component="label" fullWidth>
|
||||
{importFile ? importFile.name : "Seleziona File"}
|
||||
{importFile ? importFile.name : t("reports.selectFile")}
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
@@ -520,7 +521,7 @@ export default function ReportTemplatesPage() {
|
||||
}}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Annulla
|
||||
{t("reports.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -528,7 +529,7 @@ export default function ReportTemplatesPage() {
|
||||
disabled={!importFile || importMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{importMutation.isPending ? "Importazione..." : "Importa"}
|
||||
{importMutation.isPending ? t("reports.importing") : t("reports.import")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { risorseService, lookupService } from '../services/lookupService';
|
||||
import { Risorsa } from '../types';
|
||||
|
||||
export default function RisorsePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Risorsa>>({ attivo: true });
|
||||
@@ -80,19 +82,19 @@ export default function RisorsePage() {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'nome', headerName: 'Nome', width: 150 },
|
||||
{ field: 'cognome', headerName: 'Cognome', width: 150 },
|
||||
{ field: 'nome', headerName: t('resources.name'), width: 150 },
|
||||
{ field: 'cognome', headerName: t('resources.surname'), width: 150 },
|
||||
{
|
||||
field: 'tipoRisorsa',
|
||||
headerName: 'Tipo',
|
||||
headerName: t('resources.type'),
|
||||
width: 150,
|
||||
valueGetter: (value: any) => value?.descrizione || '',
|
||||
},
|
||||
{ field: 'telefono', headerName: 'Telefono', width: 130 },
|
||||
{ field: 'email', headerName: 'Email', flex: 1, minWidth: 200 },
|
||||
{ field: 'telefono', headerName: t('resources.phone'), width: 130 },
|
||||
{ field: 'email', headerName: t('resources.email'), flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: 'Azioni',
|
||||
headerName: t('common.actions'),
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -104,7 +106,7 @@ export default function RisorsePage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm('Eliminare questa risorsa?')) {
|
||||
if (confirm(t('resources.deleteConfirm'))) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -119,9 +121,9 @@ export default function RisorsePage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4">Risorse</Typography>
|
||||
<Typography variant="h4">{t('resources.title')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
Nuova Risorsa
|
||||
{t('resources.newResource')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -139,12 +141,12 @@ export default function RisorsePage() {
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{editingId ? 'Modifica Risorsa' : 'Nuova Risorsa'}</DialogTitle>
|
||||
<DialogTitle>{editingId ? t('resources.editResource') : t('resources.newResource')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Nome"
|
||||
label={t('resources.name')}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.nome || ''}
|
||||
@@ -153,7 +155,7 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Cognome"
|
||||
label={t('resources.surname')}
|
||||
fullWidth
|
||||
value={formData.cognome || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cognome: e.target.value })}
|
||||
@@ -161,10 +163,10 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo Risorsa</InputLabel>
|
||||
<InputLabel>{t('resources.resourceType')}</InputLabel>
|
||||
<Select
|
||||
value={formData.tipoRisorsaId || ''}
|
||||
label="Tipo Risorsa"
|
||||
label={t('resources.resourceType')}
|
||||
onChange={(e) => setFormData({ ...formData, tipoRisorsaId: e.target.value as number })}
|
||||
>
|
||||
{tipiRisorsa.map((t) => (
|
||||
@@ -175,7 +177,7 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Telefono"
|
||||
label={t('resources.phone')}
|
||||
fullWidth
|
||||
value={formData.telefono || ''}
|
||||
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
|
||||
@@ -183,7 +185,7 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Email"
|
||||
label={t('resources.email')}
|
||||
fullWidth
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
@@ -192,7 +194,7 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Note"
|
||||
label={t('common.notes')}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
@@ -203,9 +205,9 @@ export default function RisorsePage() {
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? 'Salva' : 'Crea'}
|
||||
{editingId ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -30,3 +30,45 @@ export interface CustomFieldDefinition {
|
||||
export interface CustomFieldValues {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const entityNames: Record<string, string> = {
|
||||
Client: "Clienti",
|
||||
Article: "Articoli",
|
||||
Event: "Eventi",
|
||||
WarehouseArticle: "Articoli Magazzino",
|
||||
WarehouseLocation: "Magazzini",
|
||||
Resource: "Risorse"
|
||||
};
|
||||
|
||||
export const entityIcons: Record<string, string> = {
|
||||
Client: "People",
|
||||
Article: "Restaurant",
|
||||
Event: "Event",
|
||||
WarehouseArticle: "Inventory",
|
||||
WarehouseLocation: "Warehouse",
|
||||
Resource: "Badge"
|
||||
};
|
||||
|
||||
export const fieldTypeNames: Record<number, string> = {
|
||||
[CustomFieldType.Text]: "Testo",
|
||||
[CustomFieldType.Number]: "Numero",
|
||||
[CustomFieldType.Date]: "Data",
|
||||
[CustomFieldType.Boolean]: "Booleano",
|
||||
[CustomFieldType.Select]: "Selezione",
|
||||
[CustomFieldType.MultiSelect]: "Selezione Multipla",
|
||||
[CustomFieldType.TextArea]: "Area Testo",
|
||||
[CustomFieldType.Color]: "Colore",
|
||||
[CustomFieldType.Url]: "URL",
|
||||
[CustomFieldType.Email]: "Email"
|
||||
};
|
||||
|
||||
export const groupByEntity = (fields: CustomFieldDefinition[]) => {
|
||||
return fields.reduce((acc, field) => {
|
||||
const entity = field.entityName;
|
||||
if (!acc[entity]) {
|
||||
acc[entity] = [];
|
||||
}
|
||||
acc[entity].push(field);
|
||||
return acc;
|
||||
}, {} as Record<string, CustomFieldDefinition[]>);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user