This commit is contained in:
2025-11-29 16:06:13 +01:00
parent c7dbcde5dd
commit cedcc503fa
34 changed files with 9097 additions and 191 deletions

View File

@@ -19,6 +19,7 @@ import ReportTemplatesPage from "./pages/ReportTemplatesPage";
import ReportEditorPage from "./pages/ReportEditorPage";
import ModulesAdminPage from "./pages/ModulesAdminPage";
import ModulePurchasePage from "./pages/ModulePurchasePage";
import AutoCodesAdminPage from "./pages/AutoCodesAdminPage";
import WarehouseRoutes from "./modules/warehouse/routes";
import { ModuleGuard } from "./components/ModuleGuard";
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
@@ -90,12 +91,16 @@ function App() {
path="report-editor/:id"
element={<ReportEditorPage />}
/>
{/* Moduli */}
{/* Admin */}
<Route path="modules" element={<ModulesAdminPage />} />
<Route
path="modules/purchase/:code"
element={<ModulePurchasePage />}
/>
<Route
path="admin/auto-codes"
element={<AutoCodesAdminPage />}
/>
{/* Warehouse Module */}
<Route
path="warehouse/*"

View File

@@ -29,6 +29,7 @@ import {
Close as CloseIcon,
Extension as ModulesIcon,
Warehouse as WarehouseIcon,
Code as AutoCodeIcon,
} from "@mui/icons-material";
import CollaborationIndicator from "./collaboration/CollaborationIndicator";
import { useModules } from "../contexts/ModuleContext";
@@ -52,6 +53,7 @@ const menuItems = [
},
{ 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() {

View File

@@ -86,6 +86,7 @@ export default function ArticleFormPage() {
const [tabValue, setTabValue] = useState(0);
const [formData, setFormData] = useState({
code: "",
alternativeCode: "",
description: "",
shortDescription: "",
categoryId: undefined as number | undefined,
@@ -138,6 +139,7 @@ export default function ArticleFormPage() {
if (article) {
setFormData({
code: article.code,
alternativeCode: article.alternativeCode || "",
description: article.description,
shortDescription: article.shortDescription || "",
categoryId: article.categoryId,
@@ -190,7 +192,8 @@ export default function ArticleFormPage() {
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.code.trim()) {
// Il codice è generato automaticamente, non richiede validazione in creazione
if (!isNew && !formData.code.trim()) {
newErrors.code = "Il codice è obbligatorio";
}
if (!formData.description.trim()) {
@@ -211,9 +214,10 @@ export default function ArticleFormPage() {
let savedId: number;
if (isNew) {
const createData: CreateArticleDto = {
code: formData.code,
// code è generato automaticamente dal backend
description: formData.description,
shortDescription: formData.shortDescription || undefined,
alternativeCode: formData.alternativeCode || undefined,
categoryId: formData.categoryId,
unitOfMeasure: formData.unitOfMeasure,
barcode: formData.barcode || undefined,
@@ -234,9 +238,10 @@ export default function ArticleFormPage() {
savedId = result.id;
} else {
const updateData: UpdateArticleDto = {
code: formData.code,
// code non modificabile
description: formData.description,
shortDescription: formData.shortDescription || undefined,
alternativeCode: formData.alternativeCode || undefined,
categoryId: formData.categoryId,
unitOfMeasure: formData.unitOfMeasure,
barcode: formData.barcode || undefined,
@@ -327,19 +332,46 @@ export default function ArticleFormPage() {
Informazioni Base
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 4 }}>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
fullWidth
label="Codice"
value={formData.code}
onChange={(e) => handleChange("code", e.target.value)}
error={!!errors.code}
helperText={errors.code}
required
disabled={!isNew}
value={
isNew ? "(Generato al salvataggio)" : formData.code
}
disabled
helperText={
isNew
? "Verrà assegnato automaticamente"
: "Generato automaticamente"
}
InputProps={{
readOnly: true,
}}
sx={
isNew
? {
"& .MuiInputBase-input.Mui-disabled": {
fontStyle: "italic",
color: "text.secondary",
},
}
: undefined
}
/>
</Grid>
<Grid size={{ xs: 12, sm: 8 }}>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
fullWidth
label="Codice Alternativo"
value={formData.alternativeCode}
onChange={(e) =>
handleChange("alternativeCode", e.target.value)
}
helperText="Opzionale"
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
label="Descrizione"

View File

@@ -47,6 +47,7 @@ import {
const initialFormData = {
code: "",
alternativeCode: "",
name: "",
description: "",
type: WarehouseType.Physical,
@@ -77,6 +78,7 @@ export default function WarehouseLocationsPage() {
setEditingWarehouse(warehouse);
setFormData({
code: warehouse.code,
alternativeCode: warehouse.alternativeCode || "",
name: warehouse.name,
description: warehouse.description || "",
type: warehouse.type,
@@ -109,9 +111,7 @@ export default function WarehouseLocationsPage() {
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.code.trim()) {
newErrors.code = "Il codice è obbligatorio";
}
// Il codice è generato automaticamente, non richiede validazione in creazione
if (!formData.name.trim()) {
newErrors.name = "Il nome è obbligatorio";
}
@@ -124,12 +124,16 @@ export default function WarehouseLocationsPage() {
try {
if (editingWarehouse) {
// In modifica non inviamo il code (non modificabile)
const { code: _code, ...updateData } = formData;
await updateMutation.mutateAsync({
id: editingWarehouse.id,
data: formData,
data: updateData,
});
} else {
await createMutation.mutateAsync(formData);
// In creazione non inviamo il code (generato automaticamente dal backend)
const { code: _code, ...createData } = formData;
await createMutation.mutateAsync(createData);
}
handleCloseDialog();
} catch (error) {
@@ -364,19 +368,46 @@ export default function WarehouseLocationsPage() {
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs: 12, sm: 4 }}>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
fullWidth
label="Codice"
value={formData.code}
onChange={(e) => handleChange("code", e.target.value)}
error={!!errors.code}
helperText={errors.code}
required
disabled={!!editingWarehouse}
value={
editingWarehouse ? formData.code : "(Generato al salvataggio)"
}
disabled
helperText={
editingWarehouse
? "Generato automaticamente"
: "Verrà assegnato automaticamente"
}
InputProps={{
readOnly: true,
}}
sx={
!editingWarehouse
? {
"& .MuiInputBase-input.Mui-disabled": {
fontStyle: "italic",
color: "text.secondary",
},
}
: undefined
}
/>
</Grid>
<Grid size={{ xs: 12, sm: 8 }}>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
fullWidth
label="Codice Alternativo"
value={formData.alternativeCode}
onChange={(e) =>
handleChange("alternativeCode", e.target.value)
}
helperText="Opzionale"
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
label="Nome"

View File

@@ -117,6 +117,7 @@ export enum InventoryStatus {
export interface WarehouseLocationDto {
id: number;
code: string;
alternativeCode?: string;
name: string;
description?: string;
address?: string;
@@ -134,7 +135,8 @@ export interface WarehouseLocationDto {
}
export interface CreateWarehouseDto {
code: string;
// code è generato automaticamente dal backend
alternativeCode?: string;
name: string;
description?: string;
address?: string;
@@ -148,7 +150,20 @@ export interface CreateWarehouseDto {
notes?: string;
}
export interface UpdateWarehouseDto extends CreateWarehouseDto {
export interface UpdateWarehouseDto {
// code non è modificabile
alternativeCode?: string;
name: string;
description?: string;
address?: string;
city?: string;
province?: string;
postalCode?: string;
country?: string;
type: WarehouseType;
isDefault: boolean;
sortOrder: number;
notes?: string;
isActive: boolean;
}
@@ -159,6 +174,7 @@ export interface UpdateWarehouseDto extends CreateWarehouseDto {
export interface CategoryDto {
id: number;
code: string;
alternativeCode?: string;
name: string;
description?: string;
parentCategoryId?: number;
@@ -189,7 +205,8 @@ export interface CategoryTreeDto {
}
export interface CreateCategoryDto {
code: string;
// code è generato automaticamente dal backend
alternativeCode?: string;
name: string;
description?: string;
parentCategoryId?: number;
@@ -201,7 +218,8 @@ export interface CreateCategoryDto {
}
export interface UpdateCategoryDto {
code: string;
// code non è modificabile
alternativeCode?: string;
name: string;
description?: string;
icon?: string;
@@ -219,6 +237,7 @@ export interface UpdateCategoryDto {
export interface ArticleDto {
id: number;
code: string;
alternativeCode?: string;
description: string;
shortDescription?: string;
barcode?: string;
@@ -253,9 +272,10 @@ export interface ArticleDto {
}
export interface CreateArticleDto {
code: string;
// code è generato automaticamente dal backend
description: string;
shortDescription?: string;
alternativeCode?: string;
barcode?: string;
manufacturerCode?: string;
categoryId?: number;
@@ -280,7 +300,33 @@ export interface CreateArticleDto {
notes?: string;
}
export interface UpdateArticleDto extends CreateArticleDto {
export interface UpdateArticleDto {
// code non è modificabile
description: string;
shortDescription?: string;
alternativeCode?: string;
barcode?: string;
manufacturerCode?: string;
categoryId?: number;
unitOfMeasure: string;
secondaryUnitOfMeasure?: string;
unitConversionFactor?: number;
stockManagement: StockManagementType;
isBatchManaged: boolean;
isSerialManaged: boolean;
hasExpiry: boolean;
expiryWarningDays?: number;
minimumStock?: number;
maximumStock?: number;
reorderPoint?: number;
reorderQuantity?: number;
leadTimeDays?: number;
valuationMethod?: ValuationMethod;
standardCost?: number;
baseSellingPrice?: number;
weight?: number;
volume?: number;
notes?: string;
isActive: boolean;
}
@@ -816,7 +862,7 @@ export function formatCurrency(value: number | undefined | null): string {
export function formatQuantity(
value: number | undefined | null,
decimals: number = 2
decimals: number = 2,
): string {
if (value === undefined || value === null) return "-";
return new Intl.NumberFormat("it-IT", {

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Box,
Typography,
@@ -16,11 +16,15 @@ import {
InputLabel,
Select,
MenuItem,
} 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 { articoliService, lookupService } from '../services/lookupService';
import { Articolo } from '../types';
} 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 { articoliService, lookupService } from "../services/lookupService";
import { Articolo } from "../types";
export default function ArticoliPage() {
const queryClient = useQueryClient();
@@ -29,39 +33,40 @@ export default function ArticoliPage() {
const [formData, setFormData] = useState<Partial<Articolo>>({ attivo: true });
const { data: articoli = [], isLoading } = useQuery({
queryKey: ['articoli'],
queryKey: ["articoli"],
queryFn: () => articoliService.getAll(),
});
const { data: tipiMateriale = [] } = useQuery({
queryKey: ['lookup', 'tipi-materiale'],
queryKey: ["lookup", "tipi-materiale"],
queryFn: () => lookupService.getTipiMateriale(),
});
const { data: categorie = [] } = useQuery({
queryKey: ['lookup', 'categorie'],
queryKey: ["lookup", "categorie"],
queryFn: () => lookupService.getCategorie(),
});
const createMutation = useMutation({
mutationFn: (data: Partial<Articolo>) => articoliService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['articoli'] });
queryClient.invalidateQueries({ queryKey: ["articoli"] });
handleCloseDialog();
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<Articolo> }) => articoliService.update(id, data),
mutationFn: ({ id, data }: { id: number; data: Partial<Articolo> }) =>
articoliService.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['articoli'] });
queryClient.invalidateQueries({ queryKey: ["articoli"] });
handleCloseDialog();
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => articoliService.delete(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['articoli'] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["articoli"] }),
});
const handleCloseDialog = () => {
@@ -78,35 +83,45 @@ export default function ArticoliPage() {
const handleSubmit = () => {
if (editingId) {
updateMutation.mutate({ id: editingId, data: formData });
// In modifica, non inviamo il codice (non modificabile)
const { codice: _codice, ...updateData } = formData;
updateMutation.mutate({ id: editingId, data: updateData });
} else {
createMutation.mutate(formData);
// In creazione, non inviamo il codice (generato automaticamente)
const { codice: _codice, ...createData } = formData;
createMutation.mutate(createData);
}
};
const columns: GridColDef[] = [
{ field: 'codice', headerName: 'Codice', width: 100 },
{ field: 'descrizione', headerName: 'Descrizione', flex: 1, minWidth: 200 },
{ field: "codice", headerName: "Codice", width: 100 },
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
{ field: "descrizione", headerName: "Descrizione", flex: 1, minWidth: 200 },
{
field: 'tipoMateriale',
headerName: 'Tipo',
field: "tipoMateriale",
headerName: "Tipo",
width: 130,
valueGetter: (value: any) => value?.descrizione || '',
valueGetter: (value: any) => value?.descrizione || "",
},
{
field: 'categoria',
headerName: 'Categoria',
field: "categoria",
headerName: "Categoria",
width: 120,
valueGetter: (value: any) => value?.descrizione || '',
valueGetter: (value: any) => value?.descrizione || "",
},
{ field: 'qtaDisponibile', headerName: 'Disponibile', 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: 'actions',
headerName: 'Azioni',
field: "qtaDisponibile",
headerName: "Disponibile",
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: "actions",
headerName: "Azioni",
width: 120,
sortable: false,
renderCell: (params) => (
@@ -118,7 +133,7 @@ export default function ArticoliPage() {
size="small"
color="error"
onClick={() => {
if (confirm('Eliminare questo articolo?')) {
if (confirm("Eliminare questo articolo?")) {
deleteMutation.mutate(params.row.id);
}
}}
@@ -132,14 +147,25 @@ export default function ArticoliPage() {
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h4">Articoli</Typography>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setOpenDialog(true)}
>
Nuovo Articolo
</Button>
</Box>
<Paper sx={{ height: 600, width: '100%' }}>
<Paper sx={{ height: 600, width: "100%" }}>
<DataGrid
rows={articoli}
columns={columns}
@@ -152,38 +178,89 @@ export default function ArticoliPage() {
/>
</Paper>
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
<DialogTitle>{editingId ? 'Modifica Articolo' : 'Nuovo Articolo'}</DialogTitle>
<Dialog
open={openDialog}
onClose={handleCloseDialog}
maxWidth="md"
fullWidth
>
<DialogTitle>
{editingId ? "Modifica Articolo" : "Nuovo Articolo"}
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs: 12, md: 4 }}>
<Grid size={{ xs: 12, md: 3 }}>
<TextField
label="Codice"
fullWidth
required
value={formData.codice || ''}
onChange={(e) => setFormData({ ...formData, codice: e.target.value })}
value={
editingId
? formData.codice || ""
: "(Generato al salvataggio)"
}
disabled
helperText={
editingId
? "Generato automaticamente"
: "Verrà assegnato automaticamente"
}
InputProps={{
readOnly: true,
}}
sx={
!editingId
? {
"& .MuiInputBase-input.Mui-disabled": {
fontStyle: "italic",
color: "text.secondary",
},
}
: undefined
}
/>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<Grid size={{ xs: 12, md: 3 }}>
<TextField
label="Codice Alternativo"
fullWidth
value={formData.codiceAlternativo || ""}
onChange={(e) =>
setFormData({
...formData,
codiceAlternativo: e.target.value,
})
}
helperText="Opzionale"
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Descrizione"
fullWidth
required
value={formData.descrizione || ''}
onChange={(e) => setFormData({ ...formData, descrizione: e.target.value })}
value={formData.descrizione || ""}
onChange={(e) =>
setFormData({ ...formData, descrizione: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<FormControl fullWidth>
<InputLabel>Tipo Materiale</InputLabel>
<Select
value={formData.tipoMaterialeId || ''}
value={formData.tipoMaterialeId || ""}
label="Tipo Materiale"
onChange={(e) => setFormData({ ...formData, tipoMaterialeId: e.target.value as number })}
onChange={(e) =>
setFormData({
...formData,
tipoMaterialeId: e.target.value as number,
})
}
>
{tipiMateriale.map((t) => (
<MenuItem key={t.id} value={t.id}>{t.descrizione}</MenuItem>
<MenuItem key={t.id} value={t.id}>
{t.descrizione}
</MenuItem>
))}
</Select>
</FormControl>
@@ -192,12 +269,19 @@ export default function ArticoliPage() {
<FormControl fullWidth>
<InputLabel>Categoria</InputLabel>
<Select
value={formData.categoriaId || ''}
value={formData.categoriaId || ""}
label="Categoria"
onChange={(e) => setFormData({ ...formData, categoriaId: e.target.value as number })}
onChange={(e) =>
setFormData({
...formData,
categoriaId: e.target.value as number,
})
}
>
{categorie.map((c) => (
<MenuItem key={c.id} value={c.id}>{c.descrizione}</MenuItem>
<MenuItem key={c.id} value={c.id}>
{c.descrizione}
</MenuItem>
))}
</Select>
</FormControl>
@@ -207,16 +291,23 @@ export default function ArticoliPage() {
label="Quantità Disponibile"
fullWidth
type="number"
value={formData.qtaDisponibile || ''}
onChange={(e) => setFormData({ ...formData, qtaDisponibile: parseFloat(e.target.value) || undefined })}
value={formData.qtaDisponibile || ""}
onChange={(e) =>
setFormData({
...formData,
qtaDisponibile: parseFloat(e.target.value) || undefined,
})
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<TextField
label="Unità Misura"
fullWidth
value={formData.unitaMisura || ''}
onChange={(e) => setFormData({ ...formData, unitaMisura: e.target.value })}
value={formData.unitaMisura || ""}
onChange={(e) =>
setFormData({ ...formData, unitaMisura: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}></Grid>
@@ -225,8 +316,13 @@ export default function ArticoliPage() {
label="Qta Std Adulti (A)"
fullWidth
type="number"
value={formData.qtaStdA || ''}
onChange={(e) => setFormData({ ...formData, qtaStdA: parseFloat(e.target.value) || undefined })}
value={formData.qtaStdA || ""}
onChange={(e) =>
setFormData({
...formData,
qtaStdA: parseFloat(e.target.value) || undefined,
})
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
@@ -234,8 +330,13 @@ export default function ArticoliPage() {
label="Qta Std Buffet (B)"
fullWidth
type="number"
value={formData.qtaStdB || ''}
onChange={(e) => setFormData({ ...formData, qtaStdB: parseFloat(e.target.value) || undefined })}
value={formData.qtaStdB || ""}
onChange={(e) =>
setFormData({
...formData,
qtaStdB: parseFloat(e.target.value) || undefined,
})
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
@@ -243,8 +344,13 @@ export default function ArticoliPage() {
label="Qta Std Seduti (S)"
fullWidth
type="number"
value={formData.qtaStdS || ''}
onChange={(e) => setFormData({ ...formData, qtaStdS: parseFloat(e.target.value) || undefined })}
value={formData.qtaStdS || ""}
onChange={(e) =>
setFormData({
...formData,
qtaStdS: parseFloat(e.target.value) || undefined,
})
}
/>
</Grid>
<Grid size={{ xs: 12 }}>
@@ -253,8 +359,10 @@ export default function ArticoliPage() {
fullWidth
multiline
rows={3}
value={formData.note || ''}
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
value={formData.note || ""}
onChange={(e) =>
setFormData({ ...formData, note: e.target.value })
}
/>
</Grid>
</Grid>
@@ -262,7 +370,7 @@ export default function ArticoliPage() {
<DialogActions>
<Button onClick={handleCloseDialog}>Annulla</Button>
<Button variant="contained" onClick={handleSubmit}>
{editingId ? 'Salva' : 'Crea'}
{editingId ? "Salva" : "Crea"}
</Button>
</DialogActions>
</Dialog>

View File

@@ -0,0 +1,793 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Box,
Card,
CardContent,
Typography,
Button,
Chip,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Alert,
CircularProgress,
Tooltip,
LinearProgress,
Switch,
FormControlLabel,
TextField,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
FormControl,
InputLabel,
Select,
MenuItem,
InputAdornment,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import {
Refresh as RefreshIcon,
Edit as EditIcon,
RestartAlt as ResetIcon,
ExpandMore as ExpandMoreIcon,
Code as CodeIcon,
Preview as PreviewIcon,
Help as HelpIcon,
ContentCopy as CopyIcon,
} from "@mui/icons-material";
import * as Icons from "@mui/icons-material";
import { autoCodeService } from "../services/autoCodeService";
import type {
AutoCodeDto,
AutoCodeUpdateDto,
PlaceholderInfo,
} from "../types/autoCode";
import { groupByModule, moduleNames, moduleIcons } from "../types/autoCode";
export default function AutoCodesAdminPage() {
const queryClient = useQueryClient();
const [editingConfig, setEditingConfig] = useState<AutoCodeDto | null>(null);
const [confirmReset, setConfirmReset] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null);
const [showHelp, setShowHelp] = useState(false);
const [expandedModule, setExpandedModule] = useState<string | false>("core");
// Query per tutte le configurazioni
const {
data: configs = [],
isLoading,
refetch,
} = useQuery({
queryKey: ["autocodes"],
queryFn: () => autoCodeService.getAll(),
});
// Query per i placeholder disponibili
const { data: placeholders = [] } = useQuery({
queryKey: ["autocodes", "placeholders"],
queryFn: () => autoCodeService.getPlaceholders(),
});
// Mutation per aggiornare configurazione
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: AutoCodeUpdateDto }) =>
autoCodeService.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["autocodes"] });
setEditingConfig(null);
},
});
// Mutation per reset sequenza
const resetMutation = useMutation({
mutationFn: (entityCode: string) =>
autoCodeService.resetSequence(entityCode),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["autocodes"] });
setConfirmReset(null);
},
});
// Mutation per preview codice
const previewMutation = useMutation({
mutationFn: (entityCode: string) => autoCodeService.previewCode(entityCode),
onSuccess: (data) => {
setPreviewCode(data.code);
},
});
// Raggruppa configurazioni per modulo
const groupedConfigs = groupByModule(configs);
// Helper per ottenere icona modulo
const getModuleIcon = (moduleCode: string) => {
const iconName = moduleIcons[moduleCode] || "Extension";
const IconComponent = (Icons as Record<string, React.ComponentType>)[
iconName
];
return IconComponent ? <IconComponent /> : <Icons.Extension />;
};
if (isLoading) {
return (
<Box sx={{ p: 3 }}>
<LinearProgress />
</Box>
);
}
return (
<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>
Codici Automatici
</Typography>
<Typography color="text.secondary">
Configura i pattern per la generazione automatica dei codici
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1 }}>
<Button
variant="outlined"
startIcon={<HelpIcon />}
onClick={() => setShowHelp(true)}
>
Guida Pattern
</Button>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => refetch()}
>
Aggiorna
</Button>
</Box>
</Box>
{/* Accordion per moduli */}
{Object.entries(groupedConfigs).map(([moduleCode, moduleConfigs]) => (
<Accordion
key={moduleCode}
expanded={expandedModule === moduleCode}
onChange={(_, isExpanded) =>
setExpandedModule(isExpanded ? moduleCode : false)
}
sx={{ mb: 1 }}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{getModuleIcon(moduleCode)}
<Typography variant="h6">
{moduleNames[moduleCode] || moduleCode}
</Typography>
<Chip
label={`${moduleConfigs.length} configurazioni`}
size="small"
variant="outlined"
/>
</Box>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper} variant="outlined">
<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>
</TableRow>
</TableHead>
<TableBody>
{moduleConfigs.map((config) => (
<TableRow key={config.id} hover>
<TableCell>
<Box>
<Typography variant="body2" fontWeight="medium">
{config.entityName}
</Typography>
<Typography variant="caption" color="text.secondary">
{config.entityCode}
</Typography>
</Box>
</TableCell>
<TableCell>
<Chip
label={config.prefix || "-"}
size="small"
variant="outlined"
/>
</TableCell>
<TableCell>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", fontSize: "0.85rem" }}
>
{config.pattern}
</Typography>
</TableCell>
<TableCell>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 0.5,
}}
>
<Typography
variant="body2"
sx={{
fontFamily: "monospace",
color: "primary.main",
fontWeight: "medium",
}}
>
{config.exampleCode}
</Typography>
<Tooltip title="Anteprima prossimo codice">
<IconButton
size="small"
onClick={() =>
previewMutation.mutate(config.entityCode)
}
disabled={!config.isEnabled}
>
<PreviewIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</TableCell>
<TableCell>
<Typography variant="body2">
{config.lastSequence}
</Typography>
</TableCell>
<TableCell>
{config.resetSequenceMonthly ? (
<Chip label="Mensile" size="small" color="info" />
) : config.resetSequenceYearly ? (
<Chip label="Annuale" size="small" color="warning" />
) : (
<Chip label="Mai" size="small" variant="outlined" />
)}
</TableCell>
<TableCell align="center">
<Chip
label={config.isEnabled ? "Attivo" : "Disattivo"}
size="small"
color={config.isEnabled ? "success" : "default"}
/>
</TableCell>
<TableCell align="right">
<Tooltip title="Modifica">
<IconButton
size="small"
onClick={() => setEditingConfig(config)}
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Reset sequenza">
<IconButton
size="small"
onClick={() => setConfirmReset(config.entityCode)}
disabled={!config.isEnabled}
>
<ResetIcon fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))}
{/* Dialog modifica configurazione */}
<EditConfigDialog
config={editingConfig}
placeholders={placeholders}
onClose={() => setEditingConfig(null)}
onSave={(data) => {
if (editingConfig) {
updateMutation.mutate({ id: editingConfig.id, data });
}
}}
isSaving={updateMutation.isPending}
error={updateMutation.error as Error | null}
/>
{/* Dialog conferma reset */}
<Dialog
open={!!confirmReset}
onClose={() => setConfirmReset(null)}
maxWidth="xs"
fullWidth
>
<DialogTitle>Conferma Reset Sequenza</DialogTitle>
<DialogContent>
<Typography>
Sei sicuro di voler resettare la sequenza per{" "}
<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.
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmReset(null)}>Annulla</Button>
<Button
color="warning"
variant="contained"
onClick={() => confirmReset && resetMutation.mutate(confirmReset)}
disabled={resetMutation.isPending}
startIcon={
resetMutation.isPending ? (
<CircularProgress size={16} color="inherit" />
) : (
<ResetIcon />
)
}
>
Reset
</Button>
</DialogActions>
</Dialog>
{/* Dialog anteprima codice */}
<Dialog
open={!!previewCode}
onClose={() => setPreviewCode(null)}
maxWidth="xs"
fullWidth
>
<DialogTitle>Anteprima Prossimo Codice</DialogTitle>
<DialogContent>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 3,
bgcolor: "grey.100",
borderRadius: 1,
gap: 1,
}}
>
<CodeIcon color="primary" />
<Typography
variant="h5"
sx={{ fontFamily: "monospace", fontWeight: "bold" }}
>
{previewCode}
</Typography>
<Tooltip title="Copia">
<IconButton
size="small"
onClick={() => {
navigator.clipboard.writeText(previewCode || "");
}}
>
<CopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Typography
variant="caption"
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.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setPreviewCode(null)}>Chiudi</Button>
</DialogActions>
</Dialog>
{/* Dialog guida pattern */}
<Dialog
open={showHelp}
onClose={() => setShowHelp(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Guida ai Pattern</DialogTitle>
<DialogContent dividers>
<Typography variant="body2" paragraph>
I pattern definiscono come vengono generati i codici automatici.
Puoi combinare testo statico e placeholder dinamici.
</Typography>
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
Placeholder Disponibili
</Typography>
<TableContainer component={Paper} variant="outlined" sx={{ mb: 3 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Placeholder</TableCell>
<TableCell>Descrizione</TableCell>
<TableCell>Esempio</TableCell>
</TableRow>
</TableHead>
<TableBody>
{placeholders.map((p) => (
<TableRow key={p.placeholder}>
<TableCell>
<Typography sx={{ fontFamily: "monospace" }}>
{p.placeholder}
</Typography>
</TableCell>
<TableCell>{p.description}</TableCell>
<TableCell>
<Typography sx={{ fontFamily: "monospace" }}>
{p.example}
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Typography variant="subtitle2" gutterBottom>
Esempi di Pattern
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", mb: 1 }}
>
{"{PREFIX}-{SEQ:5}"}
</Typography>
<Typography variant="caption" color="text.secondary">
Risultato: ART-00001, ART-00002, ...
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", mb: 1 }}
>
{"{PREFIX}{YYYY}-{SEQ:5}"}
</Typography>
<Typography variant="caption" color="text.secondary">
Risultato: EVT2025-00001 (reset annuale)
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", mb: 1 }}
>
{"{PREFIX}{YY}{MM}-{SEQ:4}"}
</Typography>
<Typography variant="caption" color="text.secondary">
Risultato: MOV2511-0001 (reset mensile)
</Typography>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Card variant="outlined">
<CardContent>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", mb: 1 }}
>
{"FT{YYYY}/{SEQ:5}"}
</Typography>
<Typography variant="caption" color="text.secondary">
Risultato: FT2025/00001 (formato fattura)
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowHelp(false)}>Chiudi</Button>
</DialogActions>
</Dialog>
</Box>
);
}
// Dialog per modifica configurazione
interface EditConfigDialogProps {
config: AutoCodeDto | null;
placeholders: PlaceholderInfo[];
onClose: () => void;
onSave: (data: AutoCodeUpdateDto) => void;
isSaving: boolean;
error: Error | null;
}
function EditConfigDialog({
config,
placeholders,
onClose,
onSave,
isSaving,
error,
}: EditConfigDialogProps) {
const [formData, setFormData] = useState<AutoCodeUpdateDto>({});
// Reset form quando cambia config
const handleOpen = () => {
if (config) {
setFormData({
prefix: config.prefix,
pattern: config.pattern,
resetSequenceYearly: config.resetSequenceYearly,
resetSequenceMonthly: config.resetSequenceMonthly,
isEnabled: config.isEnabled,
isReadOnly: config.isReadOnly,
description: config.description,
});
}
};
const handleSave = () => {
onSave(formData);
};
// Calcola esempio in tempo reale
const getExampleFromPattern = (pattern: string, prefix: string | null) => {
const now = new Date();
return pattern
.replace("{PREFIX}", prefix || "")
.replace("{YEAR}", now.getFullYear().toString())
.replace("{YYYY}", now.getFullYear().toString())
.replace("{YY}", now.getFullYear().toString().slice(-2))
.replace("{MONTH}", (now.getMonth() + 1).toString().padStart(2, "0"))
.replace("{MM}", (now.getMonth() + 1).toString().padStart(2, "0"))
.replace("{DAY}", now.getDate().toString().padStart(2, "0"))
.replace("{DD}", now.getDate().toString().padStart(2, "0"))
.replace(/\{SEQ:(\d+)\}/g, (_, digits) => "X".repeat(parseInt(digits)));
};
const currentExample = getExampleFromPattern(
formData.pattern || config?.pattern || "",
formData.prefix !== undefined ? formData.prefix : config?.prefix || null,
);
return (
<Dialog
open={!!config}
onClose={onClose}
maxWidth="sm"
fullWidth
TransitionProps={{ onEntered: handleOpen }}
>
{config && (
<>
<DialogTitle>
Modifica Configurazione: {config.entityName}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
<Grid size={{ xs: 12 }}>
<TextField
label="Prefisso"
value={formData.prefix ?? config.prefix ?? ""}
onChange={(e) =>
setFormData({ ...formData, prefix: e.target.value || null })
}
fullWidth
size="small"
helperText="Testo sostituito nel placeholder {PREFIX}"
/>
</Grid>
<Grid size={{ xs: 12 }}>
<TextField
label="Pattern"
value={formData.pattern ?? config.pattern}
onChange={(e) =>
setFormData({ ...formData, pattern: e.target.value })
}
fullWidth
size="small"
helperText="Pattern per generazione codice"
InputProps={{
sx: { fontFamily: "monospace" },
endAdornment: (
<InputAdornment position="end">
<Tooltip
title={
<Box>
{placeholders.map((p) => (
<Typography
key={p.placeholder}
variant="caption"
display="block"
>
{p.placeholder}: {p.description}
</Typography>
))}
</Box>
}
>
<HelpIcon fontSize="small" color="action" />
</Tooltip>
</InputAdornment>
),
}}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<Box
sx={{
p: 2,
bgcolor: "grey.100",
borderRadius: 1,
textAlign: "center",
}}
>
<Typography variant="caption" color="text.secondary">
Anteprima:
</Typography>
<Typography
variant="h6"
sx={{ fontFamily: "monospace", color: "primary.main" }}
>
{currentExample}
</Typography>
</Box>
</Grid>
<Grid size={{ xs: 12 }}>
<FormControl fullWidth size="small">
<InputLabel>Reset Sequenza</InputLabel>
<Select
value={
(formData.resetSequenceMonthly ??
config.resetSequenceMonthly)
? "monthly"
: (formData.resetSequenceYearly ??
config.resetSequenceYearly)
? "yearly"
: "never"
}
label="Reset Sequenza"
onChange={(e) => {
const value = e.target.value;
setFormData({
...formData,
resetSequenceYearly: value === "yearly",
resetSequenceMonthly: value === "monthly",
});
}}
>
<MenuItem value="never">Mai</MenuItem>
<MenuItem value="yearly">Ogni anno</MenuItem>
<MenuItem value="monthly">Ogni mese</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12 }}>
<TextField
label="Descrizione"
value={formData.description ?? config.description ?? ""}
onChange={(e) =>
setFormData({
...formData,
description: e.target.value || null,
})
}
fullWidth
size="small"
multiline
rows={2}
/>
</Grid>
<Grid size={{ xs: 6 }}>
<FormControlLabel
control={
<Switch
checked={formData.isEnabled ?? config.isEnabled}
onChange={(e) =>
setFormData({
...formData,
isEnabled: e.target.checked,
})
}
/>
}
label="Generazione attiva"
/>
</Grid>
<Grid size={{ xs: 6 }}>
<FormControlLabel
control={
<Switch
checked={formData.isReadOnly ?? config.isReadOnly}
onChange={(e) =>
setFormData({
...formData,
isReadOnly: e.target.checked,
})
}
/>
}
label="Codice non modificabile"
/>
</Grid>
</Grid>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error.message || "Errore durante il salvataggio"}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Annulla</Button>
<Button
variant="contained"
onClick={handleSave}
disabled={isSaving}
startIcon={
isSaving ? <CircularProgress size={16} color="inherit" /> : null
}
>
Salva
</Button>
</DialogActions>
</>
)}
</Dialog>
);
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Box,
Typography,
@@ -12,11 +12,15 @@ import {
DialogActions,
TextField,
Grid,
} 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 { clientiService } from '../services/lookupService';
import { Cliente } from '../types';
} 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 { clientiService } from "../services/lookupService";
import { Cliente } from "../types";
export default function ClientiPage() {
const queryClient = useQueryClient();
@@ -25,29 +29,30 @@ export default function ClientiPage() {
const [formData, setFormData] = useState<Partial<Cliente>>({ attivo: true });
const { data: clienti = [], isLoading } = useQuery({
queryKey: ['clienti'],
queryKey: ["clienti"],
queryFn: () => clientiService.getAll(),
});
const createMutation = useMutation({
mutationFn: (data: Partial<Cliente>) => clientiService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['clienti'] });
queryClient.invalidateQueries({ queryKey: ["clienti"] });
handleCloseDialog();
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<Cliente> }) => clientiService.update(id, data),
mutationFn: ({ id, data }: { id: number; data: Partial<Cliente> }) =>
clientiService.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['clienti'] });
queryClient.invalidateQueries({ queryKey: ["clienti"] });
handleCloseDialog();
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => clientiService.delete(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['clienti'] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["clienti"] }),
});
const handleCloseDialog = () => {
@@ -64,22 +69,33 @@ export default function ClientiPage() {
const handleSubmit = () => {
if (editingId) {
updateMutation.mutate({ id: editingId, data: formData });
// In modifica, non inviamo il codice (non modificabile)
const { codice: _codice, ...updateData } = formData;
updateMutation.mutate({ id: editingId, data: updateData });
} else {
createMutation.mutate(formData);
// In creazione, non inviamo il codice (generato automaticamente)
const { codice: _codice, ...createData } = formData;
createMutation.mutate(createData);
}
};
const columns: GridColDef[] = [
{ field: 'ragioneSociale', headerName: 'Ragione Sociale', 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: "codice", headerName: "Codice", width: 100 },
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
{
field: 'actions',
headerName: 'Azioni',
field: "ragioneSociale",
headerName: "Ragione Sociale",
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: "actions",
headerName: "Azioni",
width: 120,
sortable: false,
renderCell: (params) => (
@@ -91,7 +107,7 @@ export default function ClientiPage() {
size="small"
color="error"
onClick={() => {
if (confirm('Eliminare questo cliente?')) {
if (confirm("Eliminare questo cliente?")) {
deleteMutation.mutate(params.row.id);
}
}}
@@ -105,14 +121,25 @@ export default function ClientiPage() {
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h4">Clienti</Typography>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setOpenDialog(true)}
>
Nuovo Cliente
</Button>
</Box>
<Paper sx={{ height: 600, width: '100%' }}>
<Paper sx={{ height: 600, width: "100%" }}>
<DataGrid
rows={clienti}
columns={columns}
@@ -125,57 +152,120 @@ export default function ClientiPage() {
/>
</Paper>
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
<DialogTitle>{editingId ? 'Modifica Cliente' : 'Nuovo Cliente'}</DialogTitle>
<Dialog
open={openDialog}
onClose={handleCloseDialog}
maxWidth="md"
fullWidth
>
<DialogTitle>
{editingId ? "Modifica Cliente" : "Nuovo Cliente"}
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs: 12 }}>
<Grid size={{ xs: 12, md: 3 }}>
<TextField
label="Codice"
fullWidth
value={
editingId
? formData.codice || ""
: "(Generato al salvataggio)"
}
disabled
helperText={
editingId
? "Generato automaticamente"
: "Verrà assegnato automaticamente"
}
InputProps={{
readOnly: true,
}}
sx={
!editingId
? {
"& .MuiInputBase-input.Mui-disabled": {
fontStyle: "italic",
color: "text.secondary",
},
}
: undefined
}
/>
</Grid>
<Grid size={{ xs: 12, md: 3 }}>
<TextField
label="Codice Alternativo"
fullWidth
value={formData.codiceAlternativo || ""}
onChange={(e) =>
setFormData({
...formData,
codiceAlternativo: e.target.value,
})
}
helperText="Opzionale"
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Ragione Sociale"
fullWidth
required
value={formData.ragioneSociale || ''}
onChange={(e) => setFormData({ ...formData, ragioneSociale: e.target.value })}
value={formData.ragioneSociale || ""}
onChange={(e) =>
setFormData({ ...formData, ragioneSociale: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<TextField
label="Indirizzo"
fullWidth
value={formData.indirizzo || ''}
onChange={(e) => setFormData({ ...formData, indirizzo: e.target.value })}
value={formData.indirizzo || ""}
onChange={(e) =>
setFormData({ ...formData, indirizzo: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<TextField
label="CAP"
fullWidth
value={formData.cap || ''}
onChange={(e) => setFormData({ ...formData, cap: e.target.value })}
value={formData.cap || ""}
onChange={(e) =>
setFormData({ ...formData, cap: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<TextField
label="Città"
fullWidth
value={formData.citta || ''}
onChange={(e) => setFormData({ ...formData, citta: e.target.value })}
value={formData.citta || ""}
onChange={(e) =>
setFormData({ ...formData, citta: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<TextField
label="Provincia"
fullWidth
value={formData.provincia || ''}
onChange={(e) => setFormData({ ...formData, provincia: e.target.value })}
value={formData.provincia || ""}
onChange={(e) =>
setFormData({ ...formData, provincia: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Telefono"
fullWidth
value={formData.telefono || ''}
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
value={formData.telefono || ""}
onChange={(e) =>
setFormData({ ...formData, telefono: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
@@ -183,40 +273,53 @@ export default function ClientiPage() {
label="Email"
fullWidth
type="email"
value={formData.email || ''}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
value={formData.email || ""}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="PEC"
fullWidth
value={formData.pec || ''}
onChange={(e) => setFormData({ ...formData, pec: e.target.value })}
value={formData.pec || ""}
onChange={(e) =>
setFormData({ ...formData, pec: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Codice Fiscale"
fullWidth
value={formData.codiceFiscale || ''}
onChange={(e) => setFormData({ ...formData, codiceFiscale: e.target.value })}
value={formData.codiceFiscale || ""}
onChange={(e) =>
setFormData({ ...formData, codiceFiscale: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Partita IVA"
fullWidth
value={formData.partitaIva || ''}
onChange={(e) => setFormData({ ...formData, partitaIva: e.target.value })}
value={formData.partitaIva || ""}
onChange={(e) =>
setFormData({ ...formData, partitaIva: e.target.value })
}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
label="Codice Destinatario"
fullWidth
value={formData.codiceDestinatario || ''}
onChange={(e) => setFormData({ ...formData, codiceDestinatario: e.target.value })}
value={formData.codiceDestinatario || ""}
onChange={(e) =>
setFormData({
...formData,
codiceDestinatario: e.target.value,
})
}
/>
</Grid>
<Grid size={{ xs: 12 }}>
@@ -225,8 +328,10 @@ export default function ClientiPage() {
fullWidth
multiline
rows={3}
value={formData.note || ''}
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
value={formData.note || ""}
onChange={(e) =>
setFormData({ ...formData, note: e.target.value })
}
/>
</Grid>
</Grid>
@@ -234,7 +339,7 @@ export default function ClientiPage() {
<DialogActions>
<Button onClick={handleCloseDialog}>Annulla</Button>
<Button variant="contained" onClick={handleSubmit}>
{editingId ? 'Salva' : 'Crea'}
{editingId ? "Salva" : "Crea"}
</Button>
</DialogActions>
</Dialog>

View File

@@ -0,0 +1,97 @@
import api from "./api";
import type {
AutoCodeDto,
AutoCodeUpdateDto,
GenerateCodeResponse,
ResetSequenceRequest,
CheckUniqueResponse,
PlaceholderInfo,
} from "../types/autoCode";
/**
* Service per la gestione dei codici automatici
*/
export const autoCodeService = {
/**
* Ottiene tutte le configurazioni AutoCode
*/
getAll: async (): Promise<AutoCodeDto[]> => {
const response = await api.get("/autocodes");
return response.data;
},
/**
* Ottiene le configurazioni per un modulo specifico
*/
getByModule: async (moduleCode: string): Promise<AutoCodeDto[]> => {
const response = await api.get(`/autocodes/module/${moduleCode}`);
return response.data;
},
/**
* Ottiene una configurazione specifica per codice entita
*/
getByEntityCode: async (entityCode: string): Promise<AutoCodeDto> => {
const response = await api.get(`/autocodes/${entityCode}`);
return response.data;
},
/**
* Genera un nuovo codice per un'entita
*/
generateCode: async (entityCode: string): Promise<GenerateCodeResponse> => {
const response = await api.post(`/autocodes/${entityCode}/generate`);
return response.data;
},
/**
* Ottiene un'anteprima del prossimo codice senza incrementare la sequenza
*/
previewCode: async (entityCode: string): Promise<GenerateCodeResponse> => {
const response = await api.get(`/autocodes/${entityCode}/preview`);
return response.data;
},
/**
* Aggiorna una configurazione AutoCode
*/
update: async (id: number, data: AutoCodeUpdateDto): Promise<AutoCodeDto> => {
const response = await api.put(`/autocodes/${id}`, data);
return response.data;
},
/**
* Resetta la sequenza per un'entita
*/
resetSequence: async (
entityCode: string,
request?: ResetSequenceRequest
): Promise<{ message: string }> => {
const response = await api.post(`/autocodes/${entityCode}/reset-sequence`, request || {});
return response.data;
},
/**
* Verifica se un codice e unico per un'entita
*/
checkUnique: async (
entityCode: string,
code: string,
excludeId?: number
): Promise<CheckUniqueResponse> => {
const response = await api.get(`/autocodes/${entityCode}/check-unique`, {
params: { code, excludeId },
});
return response.data;
},
/**
* Ottiene i placeholder disponibili per i pattern
*/
getPlaceholders: async (): Promise<PlaceholderInfo[]> => {
const response = await api.get("/autocodes/placeholders");
return response.data;
},
};
export default autoCodeService;

View File

@@ -0,0 +1,94 @@
/**
* Types per il sistema di codici automatici
*/
export interface AutoCodeDto {
id: number;
entityCode: string;
entityName: string;
prefix: string | null;
pattern: string;
lastSequence: number;
resetSequenceYearly: boolean;
resetSequenceMonthly: boolean;
lastResetYear: number | null;
lastResetMonth: number | null;
isEnabled: boolean;
isReadOnly: boolean;
moduleCode: string | null;
description: string | null;
sortOrder: number;
exampleCode: string;
createdAt: string | null;
updatedAt: string | null;
}
export interface AutoCodeUpdateDto {
prefix?: string | null;
pattern?: string | null;
resetSequenceYearly?: boolean;
resetSequenceMonthly?: boolean;
isEnabled?: boolean;
isReadOnly?: boolean;
description?: string | null;
}
export interface GenerateCodeResponse {
code: string;
entityCode: string;
isPreview?: boolean;
}
export interface ResetSequenceRequest {
newValue?: number;
}
export interface CheckUniqueResponse {
isUnique: boolean;
code: string;
entityCode: string;
}
export interface PlaceholderInfo {
placeholder: string;
description: string;
example: string;
}
/**
* Raggruppa le configurazioni per modulo
*/
export function groupByModule(configs: AutoCodeDto[]): Record<string, AutoCodeDto[]> {
return configs.reduce((acc, config) => {
const module = config.moduleCode || "core";
if (!acc[module]) {
acc[module] = [];
}
acc[module].push(config);
return acc;
}, {} as Record<string, AutoCodeDto[]>);
}
/**
* Nomi visualizzati per i moduli
*/
export const moduleNames: Record<string, string> = {
core: "Sistema Base",
warehouse: "Magazzino",
purchases: "Acquisti",
sales: "Vendite",
production: "Produzione",
quality: "Qualità",
};
/**
* Icone per i moduli (nomi MUI icons)
*/
export const moduleIcons: Record<string, string> = {
core: "Settings",
warehouse: "Warehouse",
purchases: "ShoppingCart",
sales: "PointOfSale",
production: "Factory",
quality: "VerifiedUser",
};

View File

@@ -13,6 +13,8 @@ export interface BaseEntity {
}
export interface Cliente extends BaseEntity {
codice: string;
codiceAlternativo?: string;
ragioneSociale: string;
indirizzo?: string;
cap?: string;
@@ -89,6 +91,7 @@ export interface Risorsa extends BaseEntity {
export interface Articolo extends BaseEntity {
codice: string;
codiceAlternativo?: string;
descrizione: string;
tipoMaterialeId?: number;
tipoMateriale?: TipoMateriale;