-
This commit is contained in:
@@ -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/*"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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>
|
||||
|
||||
793
frontend/src/pages/AutoCodesAdminPage.tsx
Normal file
793
frontend/src/pages/AutoCodesAdminPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
97
frontend/src/services/autoCodeService.ts
Normal file
97
frontend/src/services/autoCodeService.ts
Normal 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;
|
||||
94
frontend/src/types/autoCode.ts
Normal file
94
frontend/src/types/autoCode.ts
Normal 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",
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user