Files
zentral/src/frontend/src/apps/events/pages/EventoDetailPage.tsx

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>
);
}