carico degli articoli delle macchine/parti/gruppi quando vengono eliminati.
All checks were successful
Deploy to VPS / build (push) Successful in 39s
Deploy to VPS / deploy (push) Successful in 29s

This commit is contained in:
AV77web 2026-05-31 11:54:06 +02:00
parent 573cdd476f
commit 15a08b006d
4 changed files with 233 additions and 30 deletions

View file

@ -102,7 +102,22 @@ export async function DELETE(request: Request, { params }: RouteParams) {
} }
try { try {
const deleted = await deleteMacchina(macchinaId); // Leggi il body per ottenere il magazzino di rientro (opzionale)
let idMagazzinoRientro: number | undefined;
try {
const body = await request.json();
if (body.idMagazzinoRientro) {
idMagazzinoRientro = parseInt(body.idMagazzinoRientro, 10);
if (isNaN(idMagazzinoRientro)) {
idMagazzinoRientro = undefined;
}
}
} catch {
// Body vuoto o non JSON, continua senza magazzino
}
const user = session.user.email || session.user.name || "unknown";
const deleted = await deleteMacchina(macchinaId, idMagazzinoRientro, user);
if (!deleted) { if (!deleted) {
return NextResponse.json( return NextResponse.json(

View file

@ -18,6 +18,7 @@ interface RimuoviModalState {
quantita: number; quantita: number;
opzione: "carico" | "elimina"; opzione: "carico" | "elimina";
idMagazzino: number | null; idMagazzino: number | null;
articoloEsiste: boolean;
} }
export default function ArticoliMacchinaModal({ export default function ArticoliMacchinaModal({
@ -54,14 +55,25 @@ export default function ArticoliMacchinaModal({
fetchMagazzini(); fetchMagazzini();
}, [macchinaStabilimentoId]); }, [macchinaStabilimentoId]);
const handleRimuoviClick = (articolo: ArticoloAssegnatoMacchina) => { const handleRimuoviClick = async (articolo: ArticoloAssegnatoMacchina) => {
setError(null);
// Verifica se l'articolo esiste ancora nel catalogo
let articoloEsiste = true;
try {
const res = await fetch(`/api/articoli/${articolo.id_articolo}`);
articoloEsiste = res.ok;
} catch {
articoloEsiste = false;
}
setRimuoviModal({ setRimuoviModal({
articolo, articolo,
quantita: 1, quantita: 1,
opzione: "carico", opzione: articoloEsiste ? "carico" : "elimina",
idMagazzino: null, idMagazzino: null,
articoloEsiste,
}); });
setError(null);
}; };
const handleConfermaRimozione = async () => { const handleConfermaRimozione = async () => {
@ -211,11 +223,18 @@ export default function ArticoliMacchinaModal({
/> />
</div> </div>
{!rimuoviModal.articoloEsiste && (
<div className="p-3 rounded-lg bg-yellow-900/30 border border-yellow-700 text-yellow-400 text-sm">
Questo articolo non esiste più nel catalogo. Verrà eliminato senza possibilità di rientro in magazzino.
</div>
)}
<div> <div>
<label className="block text-sm font-medium text-zinc-300 mb-2"> <label className="block text-sm font-medium text-zinc-300 mb-2">
Cosa fare con gli articoli rimossi? Cosa fare con gli articoli rimossi?
</label> </label>
<div className="space-y-2"> <div className="space-y-2">
{rimuoviModal.articoloEsiste && (
<label className="flex items-center gap-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700 cursor-pointer hover:border-teal-600"> <label className="flex items-center gap-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700 cursor-pointer hover:border-teal-600">
<input <input
type="radio" type="radio"
@ -230,7 +249,8 @@ export default function ArticoliMacchinaModal({
<p className="text-xs text-zinc-500">Gli articoli torneranno disponibili nel magazzino</p> <p className="text-xs text-zinc-500">Gli articoli torneranno disponibili nel magazzino</p>
</div> </div>
</label> </label>
<label className="flex items-center gap-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700 cursor-pointer hover:border-red-600"> )}
<label className={`flex items-center gap-3 p-3 rounded-lg bg-zinc-800 border cursor-pointer hover:border-red-600 ${!rimuoviModal.articoloEsiste ? 'border-red-600' : 'border-zinc-700'}`}>
<input <input
type="radio" type="radio"
name="opzione" name="opzione"

View file

@ -41,7 +41,14 @@ export default function MacchineTree() {
parentStabilimento: number | null; parentStabilimento: number | null;
}>({ parentId: null, parentStabilimento: null }); }>({ parentId: null, parentStabilimento: null });
const [deleteConfirm, setDeleteConfirm] = useState<{ id: number; nome: string } | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<{
id: number;
nome: string;
stabilimentoId: number | null;
hasArticoli: boolean;
idMagazzinoRientro: number | null;
} | null>(null);
const [deleteMagazzini, setDeleteMagazzini] = useState<{ id: number; descrizione: string }[]>([]);
const [dropModal, setDropModal] = useState<DropModalState | null>(null); const [dropModal, setDropModal] = useState<DropModalState | null>(null);
const [dropQuantita, setDropQuantita] = useState(1); const [dropQuantita, setDropQuantita] = useState(1);
@ -159,26 +166,76 @@ export default function MacchineTree() {
await fetchNodes(); await fetchNodes();
}; };
const handleDelete = (id: number) => { const handleDelete = async (id: number) => {
const node = nodes.find((n) => n.id === id); const node = nodes.find((n) => n.id === id);
if (node) { if (!node) return;
setDeleteConfirm({ id, nome: node.nome });
// Calcola se questa macchina o i suoi discendenti hanno articoli
const getDescendantIds = (nodeId: number): number[] => {
const children = nodes.filter((n) => n.id_padre === nodeId);
let ids = [nodeId];
for (const child of children) {
ids = [...ids, ...getDescendantIds(child.id)];
} }
return ids;
};
const allIds = getDescendantIds(id);
const hasArticoli = allIds.some((mid) => {
const arts = articlesByMacchina[mid] || [];
return arts.some((a) => a.quantita > 0);
});
// Se ci sono articoli, carica i magazzini dello stabilimento
if (hasArticoli && node.id_stabilimento) {
try {
const res = await fetch("/api/magazzini");
if (res.ok) {
const data = await res.json();
const filtered = data.magazzini.filter(
(m: { id: number; descrizione: string; id_stabilimento: number }) =>
m.id_stabilimento === node.id_stabilimento
);
setDeleteMagazzini(filtered);
}
} catch (err) {
console.error("Errore caricamento magazzini:", err);
}
}
setDeleteConfirm({
id,
nome: node.nome,
stabilimentoId: node.id_stabilimento,
hasArticoli,
idMagazzinoRientro: null,
});
}; };
const confirmDelete = async () => { const confirmDelete = async () => {
if (!deleteConfirm) return; if (!deleteConfirm) return;
// Se ci sono articoli ma non è stato selezionato un magazzino, non procedere
if (deleteConfirm.hasArticoli && !deleteConfirm.idMagazzinoRientro) {
return;
}
try { try {
const res = await fetch(`/api/macchine/${deleteConfirm.id}`, { const res = await fetch(`/api/macchine/${deleteConfirm.id}`, {
method: "DELETE", method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
idMagazzinoRientro: deleteConfirm.idMagazzinoRientro,
}),
}); });
if (!res.ok) throw new Error("Errore nell'eliminazione"); if (!res.ok) throw new Error("Errore nell'eliminazione");
await fetchNodes(); await Promise.all([fetchNodes(), fetchArticoliPerMacchina()]);
window.dispatchEvent(new CustomEvent("articoli-updated"));
} catch (err) { } catch (err) {
console.error("Errore eliminazione:", err); console.error("Errore eliminazione:", err);
} finally { } finally {
setDeleteConfirm(null); setDeleteConfirm(null);
setDeleteMagazzini([]);
} }
}; };
@ -458,24 +515,60 @@ export default function MacchineTree() {
{/* Delete Confirmation */} {/* Delete Confirmation */}
{deleteConfirm && ( {deleteConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setDeleteConfirm(null)} /> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => { setDeleteConfirm(null); setDeleteMagazzini([]); }} />
<div className="relative w-full max-w-sm bg-zinc-900 rounded-xl border border-zinc-800 shadow-2xl p-5"> <div className="relative w-full max-w-md bg-zinc-900 rounded-xl border border-zinc-800 shadow-2xl p-5">
<h3 className="text-lg font-semibold text-white mb-2">Conferma eliminazione</h3> <h3 className="text-lg font-semibold text-white mb-2">Conferma eliminazione</h3>
<p className="text-sm text-zinc-400 mb-4"> <p className="text-sm text-zinc-400 mb-4">
Sei sicuro di voler eliminare <strong className="text-white">{deleteConfirm.nome}</strong>? Sei sicuro di voler eliminare <strong className="text-white">{deleteConfirm.nome}</strong>?
<br /> <br />
<span className="text-red-400">Verranno eliminati anche tutti i sotto-elementi.</span> <span className="text-red-400">Verranno eliminati anche tutti i sotto-elementi.</span>
</p> </p>
{deleteConfirm.hasArticoli && (
<div className="mb-4 p-4 rounded-lg bg-yellow-900/20 border border-yellow-700/30">
<p className="text-sm text-yellow-300 mb-3">
Questa macchina contiene articoli assegnati. Seleziona il magazzino dove far rientrare gli articoli:
</p>
{deleteMagazzini.length === 0 ? (
<p className="text-xs text-red-400">
Nessun magazzino disponibile per questo stabilimento. Gli articoli andranno persi.
</p>
) : (
<select
value={deleteConfirm.idMagazzinoRientro || ""}
onChange={(e) => setDeleteConfirm({
...deleteConfirm,
idMagazzinoRientro: e.target.value ? parseInt(e.target.value) : null,
})}
className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500"
>
<option value="">Seleziona magazzino...</option>
{deleteMagazzini.map((mag) => (
<option key={mag.id} value={mag.id}>
{mag.descrizione}
</option>
))}
</select>
)}
{deleteMagazzini.length > 0 && !deleteConfirm.idMagazzinoRientro && (
<p className="text-xs text-yellow-400 mt-2">
Devi selezionare un magazzino per procedere
</p>
)}
</div>
)}
<div className="flex items-center justify-end gap-3"> <div className="flex items-center justify-end gap-3">
<button <button
onClick={() => setDeleteConfirm(null)} onClick={() => { setDeleteConfirm(null); setDeleteMagazzini([]); }}
className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white rounded-lg hover:bg-zinc-800 transition-colors" className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white rounded-lg hover:bg-zinc-800 transition-colors"
> >
Annulla Annulla
</button> </button>
<button <button
onClick={confirmDelete} onClick={confirmDelete}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-500 transition-colors" disabled={deleteConfirm.hasArticoli && deleteMagazzini.length > 0 && !deleteConfirm.idMagazzinoRientro}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
Elimina Elimina
</button> </button>

View file

@ -162,7 +162,82 @@ export async function updateMacchina(
return getMacchinaById(id); return getMacchinaById(id);
} }
export async function deleteMacchina(id: number): Promise<boolean> { export async function deleteMacchina(
id: number,
idMagazzinoRientro?: number,
user: string = "system"
): Promise<boolean> {
// Recupera gli articoli assegnati a questa macchina
const [articoliRows] = await db.execute<(RowDataPacket & {
idart: number;
qtain: number;
qtaout: number;
descrizione: string;
codice: string;
})[]>(
`SELECT idart, qtain, qtaout, descrizione, codice
FROM art_macchine_parti
WHERE idmacchina = :id AND (qtain - qtaout) > 0`,
{ id }
);
// Se c'è un magazzino di rientro, fai il carico degli articoli
if (idMagazzinoRientro && articoliRows.length > 0) {
for (const art of articoliRows) {
const quantita = art.qtain - art.qtaout;
if (quantita <= 0) continue;
// Verifica se l'articolo esiste ancora nel catalogo
const [existsRows] = await db.execute<(RowDataPacket & { id: number })[]>(
"SELECT id FROM articoli WHERE id = :idart LIMIT 1",
{ idart: art.idart }
);
if (existsRows.length > 0) {
// L'articolo esiste, registra il movimento di carico
await db.execute(
`INSERT INTO movimenti (id_articolo, articolo_codice, articolo_descrizione, quantita, tipo_movimento, causale, utente, id_magazzino_destinazione)
VALUES (:id_articolo, :articolo_codice, :articolo_descrizione, :quantita, 1, :causale, :utente, :id_magazzino_destinazione)`,
{
id_articolo: art.idart,
articolo_codice: art.codice,
articolo_descrizione: art.descrizione,
quantita,
causale: "Rientro da eliminazione macchina",
utente: user,
id_magazzino_destinazione: idMagazzinoRientro,
}
);
// Aggiorna la giacenza
const [existingGiacenza] = await db.execute<(RowDataPacket & { id: number })[]>(
"SELECT id FROM giacenze WHERE articolo_id = :articoloId AND id_magazzino = :magazzinoId",
{ articoloId: art.idart, magazzinoId: idMagazzinoRientro }
);
if (existingGiacenza.length > 0) {
await db.execute(
"UPDATE giacenze SET quantita = quantita + :quantita WHERE articolo_id = :articoloId AND id_magazzino = :magazzinoId",
{ quantita, articoloId: art.idart, magazzinoId: idMagazzinoRientro }
);
} else {
await db.execute(
"INSERT INTO giacenze (articolo_id, id_magazzino, quantita) VALUES (:articoloId, :magazzinoId, :quantita)",
{ articoloId: art.idart, magazzinoId: idMagazzinoRientro, quantita }
);
}
}
// Se l'articolo non esiste più, non facciamo nulla (gli articoli vengono persi)
}
}
// Elimina gli articoli assegnati a questa macchina
await db.execute(
"DELETE FROM art_macchine_parti WHERE idmacchina = :id",
{ id }
);
// Elimina ricorsivamente i figli
const [childRows] = await db.execute<MacchinaRow[]>( const [childRows] = await db.execute<MacchinaRow[]>(
"SELECT id FROM macchine_parti WHERE id_padre = :id", "SELECT id FROM macchine_parti WHERE id_padre = :id",
{ id } { id }
@ -170,7 +245,7 @@ export async function deleteMacchina(id: number): Promise<boolean> {
if (childRows.length > 0) { if (childRows.length > 0) {
for (const child of childRows) { for (const child of childRows) {
await deleteMacchina(child.id); await deleteMacchina(child.id, idMagazzinoRientro, user);
} }
} }