This commit is contained in:
2025-11-30 00:34:56 +01:00
parent 337c847d79
commit cf0c43a211
38 changed files with 4024 additions and 1325 deletions

View File

@@ -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:**

View File

@@ -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",

View File

@@ -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"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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(

View File

@@ -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 />

View File

@@ -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 => (

View File

@@ -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
View 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;

View File

@@ -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>,
)

View File

@@ -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"}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
</>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")];
}

View File

@@ -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"
/>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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[]>);
};