1184 lines
41 KiB
TypeScript
1184 lines
41 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useParams, useNavigate, useLocation } from "react-router-dom";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { signalRService } from "../../../services/signalr";
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
Grid,
|
|
TextField,
|
|
Button,
|
|
Tabs,
|
|
Tab,
|
|
Chip,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
IconButton,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Autocomplete,
|
|
} from "@mui/material";
|
|
import { DatePicker, TimePicker } from "@mui/x-date-pickers";
|
|
import {
|
|
Save as SaveIcon,
|
|
ArrowBack as BackIcon,
|
|
Add as AddIcon,
|
|
Delete as DeleteIcon,
|
|
ContentCopy as CopyIcon,
|
|
Refresh as RefreshIcon,
|
|
CheckCircle as ConfirmIcon,
|
|
Print as PrintIcon,
|
|
} from "@mui/icons-material";
|
|
import { useTranslation } from "react-i18next";
|
|
import dayjs from "dayjs";
|
|
import { eventiService } from "../../../services/eventiService";
|
|
import { lookupService } from "../../../services/lookupService";
|
|
import EventoCostiPanel from "../../../components/EventoCostiPanel";
|
|
import {
|
|
Evento,
|
|
StatoEvento,
|
|
EventoDettaglioOspiti,
|
|
EventoDettaglioPrelievo,
|
|
EventoDettaglioRisorsa,
|
|
} from "../../../types";
|
|
|
|
interface TabPanelProps {
|
|
children?: React.ReactNode;
|
|
index: number;
|
|
value: number;
|
|
}
|
|
|
|
function TabPanel(props: TabPanelProps) {
|
|
const { children, value, index, ...other } = props;
|
|
return (
|
|
<div role="tabpanel" hidden={value !== index} {...other}>
|
|
{value === index && <Box sx={{ py: 2 }}>{children}</Box>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const getStatoInfo = (stato: StatoEvento, t: any) => {
|
|
switch (stato) {
|
|
case StatoEvento.Scheda:
|
|
return { label: t("events.detail.status.draft"), color: "#CAE3FC", textColor: "#1976d2" };
|
|
case StatoEvento.Preventivo:
|
|
return { label: t("events.detail.status.quote"), color: "#ffffb8", textColor: "#ed6c02" };
|
|
case StatoEvento.Confermato:
|
|
return { label: t("events.detail.status.confirmed"), color: "#b8ffb8", textColor: "#2e7d32" };
|
|
default:
|
|
return { label: t("events.detail.status.new"), color: "#fafafa", textColor: "#666" };
|
|
}
|
|
};
|
|
|
|
export default function EventoDetailPage() {
|
|
const { t } = useTranslation();
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const queryClient = useQueryClient();
|
|
const [tabValue, setTabValue] = useState(0);
|
|
|
|
// Leggi la data passata dal calendario (se presente)
|
|
const initialData = location.state?.dataEvento
|
|
? { dataEvento: location.state.dataEvento }
|
|
: {};
|
|
|
|
const [formData, setFormData] = useState<Partial<Evento>>(initialData);
|
|
const [dialogOpen, setDialogOpen] = useState<string | null>(null);
|
|
const [dialogData, setDialogData] = useState<any>({});
|
|
const [hasChanges, setHasChanges] = useState(!!location.state?.dataEvento);
|
|
|
|
const eventoId = parseInt(id || "0");
|
|
const isNew = eventoId === 0 || isNaN(eventoId);
|
|
|
|
const { data: evento, isLoading } = useQuery({
|
|
queryKey: ["evento", eventoId],
|
|
queryFn: () => eventiService.getById(eventoId),
|
|
enabled: !isNew,
|
|
});
|
|
|
|
const { data: clienti = [] } = useQuery({
|
|
queryKey: ["lookup", "clienti"],
|
|
queryFn: () => lookupService.getClienti(),
|
|
});
|
|
|
|
const { data: locations = [] } = useQuery({
|
|
queryKey: ["lookup", "location"],
|
|
queryFn: () => lookupService.getLocation(),
|
|
});
|
|
|
|
const { data: tipiEvento = [] } = useQuery({
|
|
queryKey: ["lookup", "tipi-evento"],
|
|
queryFn: () => lookupService.getTipiEvento(),
|
|
});
|
|
|
|
const { data: tipiOspite = [] } = useQuery({
|
|
queryKey: ["lookup", "tipi-ospite"],
|
|
queryFn: () => lookupService.getTipiOspite(),
|
|
});
|
|
|
|
const { data: articoliLookup = [] } = useQuery({
|
|
queryKey: ["lookup", "articoli"],
|
|
queryFn: () => lookupService.getArticoli(),
|
|
});
|
|
|
|
const { data: risorseLookup = [] } = useQuery({
|
|
queryKey: ["lookup", "risorse"],
|
|
queryFn: () => lookupService.getRisorse(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (evento) {
|
|
setFormData({});
|
|
}
|
|
}, [evento]);
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (data: Partial<Evento>) => eventiService.create(data),
|
|
onSuccess: (newEvento) => {
|
|
navigate(`/events/list/${newEvento.id}`);
|
|
},
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: async (data: Partial<Evento>) => {
|
|
const result = await eventiService.update(eventoId, {
|
|
...evento,
|
|
...data,
|
|
});
|
|
return result;
|
|
},
|
|
onMutate: async (data) => {
|
|
// Optimistic update - aggiorna subito la UI
|
|
await queryClient.cancelQueries({ queryKey: ["evento", eventoId] });
|
|
const previous = queryClient.getQueryData(["evento", eventoId]);
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
...data,
|
|
}));
|
|
return { previous };
|
|
},
|
|
onError: (_err, _data, context) => {
|
|
// Rollback in caso di errore
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(["evento", eventoId], context.previous);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
setHasChanges(false);
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const cambiaStatoMutation = useMutation({
|
|
mutationFn: (stato: StatoEvento) =>
|
|
eventiService.cambiaStato(eventoId, stato),
|
|
onMutate: async (stato) => {
|
|
await queryClient.cancelQueries({ queryKey: ["evento", eventoId] });
|
|
const previous = queryClient.getQueryData(["evento", eventoId]);
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
stato,
|
|
}));
|
|
return { previous };
|
|
},
|
|
onError: (_err, _data, context) => {
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(["evento", eventoId], context.previous);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const duplicaMutation = useMutation({
|
|
mutationFn: () => eventiService.duplica(eventoId),
|
|
onSuccess: (newEvento) => {
|
|
navigate(`/events/list/${newEvento.id}`);
|
|
},
|
|
});
|
|
|
|
const ricalcolaQuantitaMutation = useMutation({
|
|
mutationFn: () => eventiService.ricalcolaQuantita(eventoId),
|
|
onSuccess: (updatedEvento) => {
|
|
queryClient.setQueryData(["evento", eventoId], updatedEvento);
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
// Mutations per dettagli - con update ottimistico
|
|
const addOspiteMutation = useMutation({
|
|
mutationFn: (data: Partial<EventoDettaglioOspiti>) =>
|
|
eventiService.addOspite(eventoId, data),
|
|
onSuccess: (newOspite) => {
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliOspiti: [...(old?.dettagliOspiti || []), newOspite],
|
|
}));
|
|
setDialogOpen(null);
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const deleteOspiteMutation = useMutation({
|
|
mutationFn: (id: number) => eventiService.deleteOspite(eventoId, id),
|
|
onMutate: async (id) => {
|
|
await queryClient.cancelQueries({ queryKey: ["evento", eventoId] });
|
|
const previous = queryClient.getQueryData(["evento", eventoId]);
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliOspiti:
|
|
old?.dettagliOspiti?.filter((o: any) => o.id !== id) || [],
|
|
}));
|
|
return { previous };
|
|
},
|
|
onError: (_err, _id, context) => {
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(["evento", eventoId], context.previous);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const addPrelievoMutation = useMutation({
|
|
mutationFn: (data: Partial<EventoDettaglioPrelievo>) =>
|
|
eventiService.addPrelievo(eventoId, data),
|
|
onSuccess: (newPrelievo) => {
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliPrelievo: [...(old?.dettagliPrelievo || []), newPrelievo],
|
|
}));
|
|
setDialogOpen(null);
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const deletePrelievoMutation = useMutation({
|
|
mutationFn: (id: number) => eventiService.deletePrelievo(eventoId, id),
|
|
onMutate: async (id) => {
|
|
await queryClient.cancelQueries({ queryKey: ["evento", eventoId] });
|
|
const previous = queryClient.getQueryData(["evento", eventoId]);
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliPrelievo:
|
|
old?.dettagliPrelievo?.filter((p: any) => p.id !== id) || [],
|
|
}));
|
|
return { previous };
|
|
},
|
|
onError: (_err, _id, context) => {
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(["evento", eventoId], context.previous);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const addRisorsaMutation = useMutation({
|
|
mutationFn: (data: Partial<EventoDettaglioRisorsa>) =>
|
|
eventiService.addRisorsa(eventoId, data),
|
|
onSuccess: (newRisorsa) => {
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliRisorse: [...(old?.dettagliRisorse || []), newRisorsa],
|
|
}));
|
|
setDialogOpen(null);
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
const deleteRisorsaMutation = useMutation({
|
|
mutationFn: (id: number) => eventiService.deleteRisorsa(eventoId, id),
|
|
onMutate: async (id) => {
|
|
await queryClient.cancelQueries({ queryKey: ["evento", eventoId] });
|
|
const previous = queryClient.getQueryData(["evento", eventoId]);
|
|
queryClient.setQueryData(["evento", eventoId], (old: any) => ({
|
|
...old,
|
|
dettagliRisorse:
|
|
old?.dettagliRisorse?.filter((r: any) => r.id !== id) || [],
|
|
}));
|
|
return { previous };
|
|
},
|
|
onError: (_err, _id, context) => {
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(["evento", eventoId], context.previous);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
signalRService.notifyChange("eventi", "updated", { id: eventoId });
|
|
},
|
|
});
|
|
|
|
if (isLoading && !isNew) {
|
|
return <Typography>{t("events.detail.loading")}</Typography>;
|
|
}
|
|
|
|
const data = isNew ? formData : { ...evento, ...formData };
|
|
const statoInfo = getStatoInfo(data.stato || StatoEvento.Scheda, t);
|
|
|
|
const handleFieldChange = (field: string, value: any) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (isNew) {
|
|
createMutation.mutate(formData);
|
|
} else {
|
|
updateMutation.mutate(formData);
|
|
}
|
|
};
|
|
|
|
const totaleOspiti = (evento?.dettagliOspiti || []).reduce(
|
|
(sum, o) => sum + o.quantita,
|
|
0,
|
|
);
|
|
|
|
return (
|
|
<Box>
|
|
{/* Header con colore stato */}
|
|
<Paper
|
|
sx={{
|
|
p: 2,
|
|
mb: 2,
|
|
backgroundColor: statoInfo.color,
|
|
borderLeft: `6px solid ${statoInfo.textColor}`,
|
|
}}
|
|
>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
<IconButton
|
|
onClick={() => navigate("/events/list")}
|
|
sx={{ color: statoInfo.textColor }}
|
|
>
|
|
<BackIcon />
|
|
</IconButton>
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
<Typography
|
|
variant="h5"
|
|
sx={{ color: statoInfo.textColor, fontWeight: "bold" }}
|
|
>
|
|
{statoInfo.label}
|
|
</Typography>
|
|
<Typography variant="subtitle1">
|
|
{data.codice || t("events.detail.newEvent")} -{" "}
|
|
{data.descrizione || t("events.detail.noDescription")}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: "flex", gap: 1 }}>
|
|
{!isNew && (
|
|
<>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<CopyIcon />}
|
|
onClick={() => duplicaMutation.mutate()}
|
|
size="small"
|
|
>
|
|
{t("events.detail.actions.duplicate")}
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<RefreshIcon />}
|
|
onClick={() => ricalcolaQuantitaMutation.mutate()}
|
|
size="small"
|
|
>
|
|
{t("events.detail.actions.recalculate")}
|
|
</Button>
|
|
{data.stato !== StatoEvento.Confermato && (
|
|
<Button
|
|
variant="contained"
|
|
color="success"
|
|
startIcon={<ConfirmIcon />}
|
|
onClick={() =>
|
|
cambiaStatoMutation.mutate(StatoEvento.Confermato)
|
|
}
|
|
size="small"
|
|
>
|
|
{t("events.detail.actions.confirm")}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<SaveIcon />}
|
|
onClick={handleSave}
|
|
disabled={!hasChanges && !isNew}
|
|
>
|
|
{t("events.detail.actions.save")}
|
|
</Button>
|
|
{!isNew && (
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<PrintIcon />}
|
|
onClick={() => {
|
|
window.open(
|
|
`http://localhost:5000/api/reports/evento/${eventoId}`,
|
|
"_blank",
|
|
);
|
|
}}
|
|
>
|
|
{t("events.detail.actions.print")}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
|
|
{/* Info principali evento */}
|
|
<Paper sx={{ p: 2, mb: 2 }}>
|
|
<Grid container spacing={2}>
|
|
{/* Prima riga: Data, Orari, Tipo */}
|
|
<Grid size={{ xs: 12, md: 2 }}>
|
|
<DatePicker
|
|
label={t("events.detail.fields.date")}
|
|
value={data.dataEvento ? dayjs(data.dataEvento) : null}
|
|
onChange={(date) =>
|
|
handleFieldChange("dataEvento", date?.format("YYYY-MM-DD"))
|
|
}
|
|
slotProps={{
|
|
textField: { fullWidth: true, size: "small", required: true },
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 1.5 }}>
|
|
<TimePicker
|
|
label={t("events.detail.fields.startTime")}
|
|
value={
|
|
data.oraInizio ? dayjs(`2000-01-01T${data.oraInizio}`) : null
|
|
}
|
|
onChange={(time) =>
|
|
handleFieldChange("oraInizio", time?.format("HH:mm:ss"))
|
|
}
|
|
slotProps={{ textField: { fullWidth: true, size: "small" } }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 1.5 }}>
|
|
<TimePicker
|
|
label={t("events.detail.fields.endTime")}
|
|
value={data.oraFine ? dayjs(`2000-01-01T${data.oraFine}`) : null}
|
|
onChange={(time) =>
|
|
handleFieldChange("oraFine", time?.format("HH:mm:ss"))
|
|
}
|
|
slotProps={{ textField: { fullWidth: true, size: "small" } }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 3 }}>
|
|
<FormControl fullWidth size="small">
|
|
<InputLabel>{t("events.detail.fields.type")}</InputLabel>
|
|
<Select
|
|
value={data.tipoEventoId || ""}
|
|
label={t("events.detail.fields.type")}
|
|
onChange={(e) =>
|
|
handleFieldChange("tipoEventoId", e.target.value)
|
|
}
|
|
>
|
|
{tipiEvento.map((t) => (
|
|
<MenuItem key={t.id} value={t.id}>
|
|
{t.descrizione}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 4 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.description")}
|
|
fullWidth
|
|
size="small"
|
|
value={data.descrizione || ""}
|
|
onChange={(e) => handleFieldChange("descrizione", e.target.value)}
|
|
placeholder={t("events.detail.fields.descriptionPlaceholder")}
|
|
/>
|
|
</Grid>
|
|
|
|
{/* Seconda riga: Cliente e Location */}
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Autocomplete
|
|
options={clienti}
|
|
getOptionLabel={(option) => option.ragioneSociale || ""}
|
|
value={clienti.find((c) => c.id === data.clienteId) || null}
|
|
onChange={(_, newValue) =>
|
|
handleFieldChange("clienteId", newValue?.id)
|
|
}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label={t("events.detail.fields.client")} size="small" fullWidth />
|
|
)}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Autocomplete
|
|
options={locations}
|
|
getOptionLabel={(option) => option.nome || ""}
|
|
value={locations.find((l) => l.id === data.locationId) || null}
|
|
onChange={(_, newValue) =>
|
|
handleFieldChange("locationId", newValue?.id)
|
|
}
|
|
renderInput={(params) => (
|
|
<TextField
|
|
{...params}
|
|
label={t("events.detail.fields.location")}
|
|
size="small"
|
|
fullWidth
|
|
/>
|
|
)}
|
|
/>
|
|
</Grid>
|
|
|
|
{/* Terza riga: Dati economici */}
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.totalGuests")}
|
|
type="number"
|
|
fullWidth
|
|
size="small"
|
|
value={data.numeroOspiti || totaleOspiti || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange(
|
|
"numeroOspiti",
|
|
parseInt(e.target.value) || undefined,
|
|
)
|
|
}
|
|
InputProps={{ readOnly: totaleOspiti > 0 }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.costPerPerson")}
|
|
type="number"
|
|
fullWidth
|
|
size="small"
|
|
value={data.costoPersona || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange(
|
|
"costoPersona",
|
|
parseFloat(e.target.value) || undefined,
|
|
)
|
|
}
|
|
InputProps={{
|
|
startAdornment: <Typography sx={{ mr: 0.5 }}>€</Typography>,
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.totalCost")}
|
|
type="number"
|
|
fullWidth
|
|
size="small"
|
|
value={data.costoTotale || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange(
|
|
"costoTotale",
|
|
parseFloat(e.target.value) || undefined,
|
|
)
|
|
}
|
|
InputProps={{
|
|
startAdornment: <Typography sx={{ mr: 0.5 }}>€</Typography>,
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.totalDeposits")}
|
|
fullWidth
|
|
size="small"
|
|
value={`€ ${(data.totaleAcconti || 0).toFixed(2)}`}
|
|
InputProps={{ readOnly: true }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<TextField
|
|
label={t("events.detail.fields.balance")}
|
|
fullWidth
|
|
size="small"
|
|
value={`€ ${(data.saldo || (data.costoTotale || 0) - (data.totaleAcconti || 0)).toFixed(2)}`}
|
|
InputProps={{ readOnly: true }}
|
|
sx={{
|
|
"& input": {
|
|
color: (data.saldo || 0) > 0 ? "error.main" : "success.main",
|
|
fontWeight: "bold",
|
|
},
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6, md: 2 }}>
|
|
<FormControl fullWidth size="small">
|
|
<InputLabel>{t("events.detail.fields.status")}</InputLabel>
|
|
<Select
|
|
value={data.stato ?? StatoEvento.Scheda}
|
|
label={t("events.detail.fields.status")}
|
|
onChange={(e) => {
|
|
if (!isNew) {
|
|
cambiaStatoMutation.mutate(e.target.value as StatoEvento);
|
|
} else {
|
|
handleFieldChange("stato", e.target.value);
|
|
}
|
|
}}
|
|
>
|
|
<MenuItem value={StatoEvento.Scheda}>{t("events.detail.status.draft")}</MenuItem>
|
|
<MenuItem value={StatoEvento.Preventivo}>{t("events.detail.status.quote")}</MenuItem>
|
|
<MenuItem value={StatoEvento.Confermato}>{t("events.detail.status.confirmed")}</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
</Paper>
|
|
|
|
{/* Tabs per dettagli */}
|
|
{!isNew && (
|
|
<Paper sx={{ p: 2 }}>
|
|
<Tabs
|
|
value={tabValue}
|
|
onChange={(_, v) => setTabValue(v)}
|
|
sx={{ borderBottom: 1, borderColor: "divider" }}
|
|
>
|
|
<Tab label={`${t("events.detail.tabs.guests")} (${evento?.dettagliOspiti?.length || 0})`} />
|
|
<Tab
|
|
label={`${t("events.detail.tabs.withdrawalList")} (${evento?.dettagliPrelievo?.length || 0})`}
|
|
/>
|
|
<Tab label={`${t("events.detail.tabs.resources")} (${evento?.dettagliRisorse?.length || 0})`} />
|
|
<Tab label={t("events.detail.tabs.costs")} />
|
|
<Tab label={t("events.detail.tabs.notes")} />
|
|
</Tabs>
|
|
|
|
{/* Tab Ospiti */}
|
|
<TabPanel value={tabValue} index={0}>
|
|
<Box
|
|
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
|
>
|
|
<Typography variant="subtitle2" color="textSecondary">
|
|
{t("events.detail.guestsTab.total")}: <strong>{totaleOspiti}</strong>
|
|
</Typography>
|
|
<Button
|
|
startIcon={<AddIcon />}
|
|
variant="contained"
|
|
size="small"
|
|
onClick={() => {
|
|
setDialogData({});
|
|
setDialogOpen("ospite");
|
|
}}
|
|
>
|
|
{t("events.detail.guestsTab.add")}
|
|
</Button>
|
|
</Box>
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
|
<TableCell>
|
|
<strong>{t("events.detail.guestsTab.type")}</strong>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<strong>{t("events.detail.guestsTab.quantity")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.guestsTab.notes")}</strong>
|
|
</TableCell>
|
|
<TableCell width={50}></TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{evento?.dettagliOspiti?.map((o) => (
|
|
<TableRow key={o.id} hover>
|
|
<TableCell>{o.tipoOspite?.descrizione}</TableCell>
|
|
<TableCell align="right">
|
|
<Chip label={o.quantita} color="primary" size="small" />
|
|
</TableCell>
|
|
<TableCell>{o.note}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
color="error"
|
|
onClick={() => deleteOspiteMutation.mutate(o.id)}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{(!evento?.dettagliOspiti ||
|
|
evento.dettagliOspiti.length === 0) && (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={4}
|
|
align="center"
|
|
sx={{ py: 4, color: "text.secondary" }}
|
|
>
|
|
{t("events.detail.guestsTab.empty")}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</TabPanel>
|
|
|
|
{/* Tab Lista Prelievo */}
|
|
<TabPanel value={tabValue} index={1}>
|
|
<Box
|
|
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
|
>
|
|
<Typography variant="subtitle2" color="textSecondary">
|
|
{t("events.detail.withdrawalTab.total")}:{" "}
|
|
<strong>{evento?.dettagliPrelievo?.length || 0}</strong>
|
|
</Typography>
|
|
<Button
|
|
startIcon={<AddIcon />}
|
|
variant="contained"
|
|
size="small"
|
|
onClick={() => {
|
|
setDialogData({});
|
|
setDialogOpen("prelievo");
|
|
}}
|
|
>
|
|
{t("events.detail.withdrawalTab.add")}
|
|
</Button>
|
|
</Box>
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
|
<TableCell>
|
|
<strong>{t("events.detail.withdrawalTab.code")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.withdrawalTab.article")}</strong>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<strong>{t("events.detail.withdrawalTab.qtyRequested")}</strong>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<strong>{t("events.detail.withdrawalTab.qtyCalculated")}</strong>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<strong>{t("events.detail.withdrawalTab.qtyActual")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.withdrawalTab.notes")}</strong>
|
|
</TableCell>
|
|
<TableCell width={50}></TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{evento?.dettagliPrelievo?.map((p) => (
|
|
<TableRow key={p.id} hover>
|
|
<TableCell>
|
|
<Chip
|
|
label={p.articolo?.codice}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{p.articolo?.descrizione}</TableCell>
|
|
<TableCell align="right">
|
|
{p.qtaRichiesta || "-"}
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
{p.qtaCalcolata?.toFixed(0) || "-"}
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
{p.qtaEffettiva || "-"}
|
|
</TableCell>
|
|
<TableCell>{p.note}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
color="error"
|
|
onClick={() => deletePrelievoMutation.mutate(p.id)}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{(!evento?.dettagliPrelievo ||
|
|
evento.dettagliPrelievo.length === 0) && (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
align="center"
|
|
sx={{ py: 4, color: "text.secondary" }}
|
|
>
|
|
{t("events.detail.withdrawalTab.empty")}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</TabPanel>
|
|
|
|
{/* Tab Risorse */}
|
|
<TabPanel value={tabValue} index={2}>
|
|
<Box
|
|
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
|
|
>
|
|
<Typography variant="subtitle2" color="textSecondary">
|
|
{t("events.detail.resourcesTab.total")}:{" "}
|
|
<strong>{evento?.dettagliRisorse?.length || 0}</strong>
|
|
</Typography>
|
|
<Button
|
|
startIcon={<AddIcon />}
|
|
variant="contained"
|
|
size="small"
|
|
onClick={() => {
|
|
setDialogData({});
|
|
setDialogOpen("risorsa");
|
|
}}
|
|
>
|
|
{t("events.detail.resourcesTab.add")}
|
|
</Button>
|
|
</Box>
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow sx={{ backgroundColor: "grey.100" }}>
|
|
<TableCell>
|
|
<strong>{t("events.detail.resourcesTab.resource")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("common.role")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.fields.startTime")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.fields.endTime")}</strong>
|
|
</TableCell>
|
|
<TableCell>
|
|
<strong>{t("events.detail.resourcesTab.notes")}</strong>
|
|
</TableCell>
|
|
<TableCell width={50}></TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{evento?.dettagliRisorse?.map((r) => (
|
|
<TableRow key={r.id} hover>
|
|
<TableCell>
|
|
<strong>
|
|
{r.risorsa?.nome} {r.risorsa?.cognome}
|
|
</strong>
|
|
</TableCell>
|
|
<TableCell>{r.ruolo}</TableCell>
|
|
<TableCell>{r.oraInizio}</TableCell>
|
|
<TableCell>{r.oraFine}</TableCell>
|
|
<TableCell>{r.note}</TableCell>
|
|
<TableCell>
|
|
<IconButton
|
|
size="small"
|
|
color="error"
|
|
onClick={() => deleteRisorsaMutation.mutate(r.id)}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{(!evento?.dettagliRisorse ||
|
|
evento.dettagliRisorse.length === 0) && (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={6}
|
|
align="center"
|
|
sx={{ py: 4, color: "text.secondary" }}
|
|
>
|
|
{t("events.detail.resourcesTab.empty")}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</TabPanel>
|
|
|
|
{/* Tab Costi */}
|
|
<TabPanel value={tabValue} index={3}>
|
|
<EventoCostiPanel eventoId={eventoId} />
|
|
</TabPanel>
|
|
|
|
{/* Tab Note */}
|
|
<TabPanel value={tabValue} index={4}>
|
|
<Grid container spacing={2}>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
label="Note Interne"
|
|
multiline
|
|
rows={4}
|
|
fullWidth
|
|
value={data.noteInterne || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange("noteInterne", e.target.value)
|
|
}
|
|
helperText="Visibili solo internamente"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
label="Note Cliente"
|
|
multiline
|
|
rows={4}
|
|
fullWidth
|
|
value={data.noteCliente || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange("noteCliente", e.target.value)
|
|
}
|
|
helperText="Da comunicare al cliente"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
label="Note Cucina"
|
|
multiline
|
|
rows={4}
|
|
fullWidth
|
|
value={data.noteCucina || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange("noteCucina", e.target.value)
|
|
}
|
|
helperText="Istruzioni per la cucina"
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
label="Note Allestimento"
|
|
multiline
|
|
rows={4}
|
|
fullWidth
|
|
value={data.noteAllestimento || ""}
|
|
onChange={(e) =>
|
|
handleFieldChange("noteAllestimento", e.target.value)
|
|
}
|
|
helperText="Istruzioni per l'allestimento"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
</TabPanel>
|
|
</Paper>
|
|
)}
|
|
|
|
{/* Dialog Ospite */}
|
|
<Dialog
|
|
open={dialogOpen === "ospite"}
|
|
onClose={() => setDialogOpen(null)}
|
|
maxWidth="xs"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>{t("events.detail.dialogs.addGuest")}</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>{t("events.detail.guestsTab.type")}</InputLabel>
|
|
<Select
|
|
value={dialogData.tipoOspiteId || ""}
|
|
label={t("events.detail.guestsTab.type")}
|
|
onChange={(e) =>
|
|
setDialogData({ ...dialogData, tipoOspiteId: e.target.value })
|
|
}
|
|
>
|
|
{tipiOspite.map((t) => (
|
|
<MenuItem key={t.id} value={t.id}>
|
|
{t.descrizione}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
<TextField
|
|
label={t("events.detail.guestsTab.quantity")}
|
|
type="number"
|
|
fullWidth
|
|
value={dialogData.quantita || ""}
|
|
onChange={(e) =>
|
|
setDialogData({
|
|
...dialogData,
|
|
quantita: parseInt(e.target.value),
|
|
})
|
|
}
|
|
/>
|
|
<TextField
|
|
label={t("events.detail.guestsTab.notes")}
|
|
fullWidth
|
|
multiline
|
|
rows={2}
|
|
value={dialogData.note || ""}
|
|
onChange={(e) =>
|
|
setDialogData({ ...dialogData, note: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => addOspiteMutation.mutate(dialogData)}
|
|
>
|
|
{t("events.detail.dialogs.add")}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Dialog Prelievo */}
|
|
<Dialog
|
|
open={dialogOpen === "prelievo"}
|
|
onClose={() => setDialogOpen(null)}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>{t("events.detail.dialogs.addArticle")}</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
|
<Autocomplete
|
|
options={articoliLookup}
|
|
getOptionLabel={(option) =>
|
|
`${option.codice} - ${option.descrizione}`
|
|
}
|
|
onChange={(_, newValue) =>
|
|
setDialogData({ ...dialogData, articoloId: newValue?.id })
|
|
}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label={t("events.detail.withdrawalTab.article")} fullWidth />
|
|
)}
|
|
/>
|
|
<TextField
|
|
label={t("events.detail.withdrawalTab.qtyRequested")}
|
|
type="number"
|
|
fullWidth
|
|
value={dialogData.qtaRichiesta || ""}
|
|
onChange={(e) =>
|
|
setDialogData({
|
|
...dialogData,
|
|
qtaRichiesta: parseFloat(e.target.value),
|
|
})
|
|
}
|
|
/>
|
|
<TextField
|
|
label={t("events.detail.withdrawalTab.notes")}
|
|
fullWidth
|
|
multiline
|
|
rows={2}
|
|
value={dialogData.note || ""}
|
|
onChange={(e) =>
|
|
setDialogData({ ...dialogData, note: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => addPrelievoMutation.mutate(dialogData)}
|
|
>
|
|
{t("events.detail.dialogs.add")}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
{/* Dialog Risorsa */}
|
|
<Dialog
|
|
open={dialogOpen === "risorsa"}
|
|
onClose={() => setDialogOpen(null)}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
>
|
|
<DialogTitle>{t("events.detail.dialogs.addResource")}</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
|
|
<Autocomplete
|
|
options={risorseLookup}
|
|
getOptionLabel={(option) =>
|
|
`${option.nome} ${option.cognome || ""} - ${option.tipo || ""}`
|
|
}
|
|
onChange={(_, newValue) =>
|
|
setDialogData({ ...dialogData, risorsaId: newValue?.id })
|
|
}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label={t("events.detail.resourcesTab.resource")} fullWidth />
|
|
)}
|
|
/>
|
|
<TextField
|
|
label={t("common.role")}
|
|
fullWidth
|
|
value={dialogData.ruolo || ""}
|
|
onChange={(e) =>
|
|
setDialogData({ ...dialogData, ruolo: e.target.value })
|
|
}
|
|
placeholder="es. Cameriere, Cuoco, etc."
|
|
/>
|
|
<Grid container spacing={2}>
|
|
<Grid size={{ xs: 6 }}>
|
|
<TimePicker
|
|
label={t("events.detail.fields.startTime")}
|
|
value={
|
|
dialogData.oraInizio
|
|
? dayjs(`2000-01-01T${dialogData.oraInizio}`)
|
|
: null
|
|
}
|
|
onChange={(time) =>
|
|
setDialogData({
|
|
...dialogData,
|
|
oraInizio: time?.format("HH:mm:ss"),
|
|
})
|
|
}
|
|
slotProps={{ textField: { fullWidth: true } }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6 }}>
|
|
<TimePicker
|
|
label={t("events.detail.fields.endTime")}
|
|
value={
|
|
dialogData.oraFine
|
|
? dayjs(`2000-01-01T${dialogData.oraFine}`)
|
|
: null
|
|
}
|
|
onChange={(time) =>
|
|
setDialogData({
|
|
...dialogData,
|
|
oraFine: time?.format("HH:mm:ss"),
|
|
})
|
|
}
|
|
slotProps={{ textField: { fullWidth: true } }}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
<TextField
|
|
label={t("events.detail.resourcesTab.notes")}
|
|
fullWidth
|
|
multiline
|
|
rows={2}
|
|
value={dialogData.note || ""}
|
|
onChange={(e) =>
|
|
setDialogData({ ...dialogData, note: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setDialogOpen(null)}>{t("events.detail.dialogs.cancel")}</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => addRisorsaMutation.mutate(dialogData)}
|
|
>
|
|
{t("events.detail.dialogs.add")}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
);
|
|
}
|