-
This commit is contained in:
@@ -52,6 +52,30 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
|||||||
|
|
||||||
**Lavoro completato nell'ultima sessione:**
|
**Lavoro completato nell'ultima sessione:**
|
||||||
|
|
||||||
|
- **NUOVA FEATURE: Gestione Inventario (Frontend)** - COMPLETATO
|
||||||
|
- **Obiettivo:** Interfaccia utente per la gestione completa degli inventari fisici
|
||||||
|
- **Frontend implementato:**
|
||||||
|
- `InventoryListPage.tsx` - Lista inventari con stato, filtri e indicatori di progresso
|
||||||
|
- `InventoryFormPage.tsx` - Form per creazione e modifica testata inventario (con gestione stati)
|
||||||
|
- `InventoryCountPage.tsx` - Pagina di conteggio con griglia editabile, calcolo differenze live
|
||||||
|
- Aggiornati `routes.tsx` e `pages/index.ts` per includere le nuove rotte
|
||||||
|
- **Funzionalità:**
|
||||||
|
- Creazione inventari (Completo, Parziale per categoria/magazzino)
|
||||||
|
- Workflow stati: Bozza -> In Corso -> Completato -> Confermato
|
||||||
|
- Avvio inventario: generazione automatica righe basata su giacenza teorica
|
||||||
|
- Conteggio: inserimento quantità rilevate, evidenziazione differenze
|
||||||
|
- Conferma: generazione automatica movimenti di rettifica (positivo/negativo)
|
||||||
|
- **Integrazione:**
|
||||||
|
- Utilizza `inventoryService` per comunicare con `InventoryController`
|
||||||
|
- Gestione date con `dayjs`
|
||||||
|
- UI coerente con Material-UI e DataGrid
|
||||||
|
|
||||||
|
- **FIX: Tasto Inventario in Dashboard Magazzino** - RISOLTO
|
||||||
|
- **Problema:** Il tasto "Inventario" nelle azioni rapide portava a una pagina 404 (`/warehouse/inventories/new`)
|
||||||
|
- **Causa:** Errore nel hook `useWarehouseNavigation` che usava il plurale `inventories` invece del singolare `inventory` definito nelle rotte
|
||||||
|
- **Soluzione:** Corretti i percorsi in `useWarehouseNavigation.ts` per corrispondere a `routes.tsx`
|
||||||
|
- **File modificati:** `frontend/src/modules/warehouse/hooks/useWarehouseNavigation.ts`
|
||||||
|
|
||||||
- **FIX: Campo Codice Readonly e Codice Alternativo** - COMPLETATO
|
- **FIX: Campo Codice Readonly e Codice Alternativo** - COMPLETATO
|
||||||
- **Obiettivo:** Il campo "Codice" deve essere sempre auto-generato (non modificabile), aggiungere campo "Codice Alternativo" opzionale
|
- **Obiettivo:** Il campo "Codice" deve essere sempre auto-generato (non modificabile), aggiungere campo "Codice Alternativo" opzionale
|
||||||
- **Backend modificato:**
|
- **Backend modificato:**
|
||||||
@@ -444,7 +468,7 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
|||||||
1. [x] **Implementare modulo Magazzino (warehouse)** - COMPLETATO (backend)
|
1. [x] **Implementare modulo Magazzino (warehouse)** - COMPLETATO (backend)
|
||||||
- Backend: Entities, Service, Controllers, API completi
|
- Backend: Entities, Service, Controllers, API completi
|
||||||
- Manca: Frontend (pagine React per gestione articoli, movimenti, giacenze)
|
- Manca: Frontend (pagine React per gestione articoli, movimenti, giacenze)
|
||||||
2. [ ] **Frontend modulo Magazzino** - Pagine React per warehouse
|
2. [x] **Frontend modulo Magazzino** - Pagine React per warehouse (Articoli, Movimenti, Giacenze, Inventario)
|
||||||
3. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
|
3. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
|
||||||
4. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
|
4. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
|
||||||
5. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
|
5. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
|
||||||
|
|||||||
101
frontend/src/modules/warehouse/components/WarehouseLayout.tsx
Normal file
101
frontend/src/modules/warehouse/components/WarehouseLayout.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { Box, Tabs, Tab, Paper, Typography, Breadcrumbs, Link } from "@mui/material";
|
||||||
|
import {
|
||||||
|
Dashboard as DashboardIcon,
|
||||||
|
Inventory as ArticleIcon,
|
||||||
|
Place as LocationIcon,
|
||||||
|
SwapHoriz as MovementIcon,
|
||||||
|
Assessment as StockIcon,
|
||||||
|
FactCheck as InventoryIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: "Dashboard", path: "/warehouse", icon: <DashboardIcon fontSize="small" /> },
|
||||||
|
{ label: "Articoli", path: "/warehouse/articles", icon: <ArticleIcon fontSize="small" /> },
|
||||||
|
{ label: "Magazzini", path: "/warehouse/locations", icon: <LocationIcon fontSize="small" /> },
|
||||||
|
{ label: "Movimenti", path: "/warehouse/movements", icon: <MovementIcon fontSize="small" /> },
|
||||||
|
{ label: "Giacenze", path: "/warehouse/stock", icon: <StockIcon fontSize="small" /> },
|
||||||
|
{ label: "Inventario", path: "/warehouse/inventory", icon: <InventoryIcon fontSize="small" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function WarehouseLayout() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Find the matching tab based on current path
|
||||||
|
const index = navItems.findIndex((item) => {
|
||||||
|
if (item.path === "/warehouse") {
|
||||||
|
return location.pathname === "/warehouse";
|
||||||
|
}
|
||||||
|
return location.pathname.startsWith(item.path);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
setValue(index);
|
||||||
|
} else {
|
||||||
|
// If no match (e.g. sub-pages), keep the closest parent or default
|
||||||
|
// Logic could be improved here but keeping it simple for now
|
||||||
|
if (location.pathname.includes("articles")) setValue(1);
|
||||||
|
else if (location.pathname.includes("locations")) setValue(2);
|
||||||
|
else if (location.pathname.includes("movements")) setValue(3);
|
||||||
|
else if (location.pathname.includes("stock")) setValue(4);
|
||||||
|
else if (location.pathname.includes("inventory")) setValue(5);
|
||||||
|
else setValue(0);
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setValue(newValue);
|
||||||
|
navigate(navItems[newValue].path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", height: "100%", gap: 2 }}>
|
||||||
|
{/* Header & Navigation */}
|
||||||
|
<Paper sx={{ px: 2, pt: 2, pb: 0 }}>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="h5" component="h1" fontWeight="bold" gutterBottom>
|
||||||
|
Gestione Magazzino
|
||||||
|
</Typography>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Typography color="text.primary">Magazzino</Typography>
|
||||||
|
{navItems[value]?.label !== "Dashboard" && (
|
||||||
|
<Typography color="text.primary">{navItems[value]?.label}</Typography>
|
||||||
|
)}
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
aria-label="warehouse navigation tabs"
|
||||||
|
sx={{ borderBottom: 1, borderColor: "divider" }}
|
||||||
|
>
|
||||||
|
{navItems.map((item, index) => (
|
||||||
|
<Tab
|
||||||
|
key={item.path}
|
||||||
|
label={item.label}
|
||||||
|
icon={item.icon}
|
||||||
|
iconPosition="start"
|
||||||
|
id={`warehouse-tab-${index}`}
|
||||||
|
aria-controls={`warehouse-tabpanel-${index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<Box sx={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
||||||
|
<Outlet />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -104,15 +104,15 @@ export function useWarehouseNavigation() {
|
|||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
const goToInventories = useCallback(() => {
|
const goToInventories = useCallback(() => {
|
||||||
navigate('/warehouse/inventories');
|
navigate('/warehouse/inventory');
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const goToInventory = useCallback((id: number) => {
|
const goToInventory = useCallback((id: number) => {
|
||||||
navigate(`/warehouse/inventories/${id}`);
|
navigate(`/warehouse/inventory/${id}`);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const goToNewInventory = useCallback(() => {
|
const goToNewInventory = useCallback(() => {
|
||||||
navigate('/warehouse/inventories/new');
|
navigate('/warehouse/inventory/new');
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
|
|||||||
333
frontend/src/modules/warehouse/pages/InventoryCountPage.tsx
Normal file
333
frontend/src/modules/warehouse/pages/InventoryCountPage.tsx
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Chip,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogActions,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridRenderCellParams,
|
||||||
|
GridToolbar,
|
||||||
|
GridRowModel,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import {
|
||||||
|
PlayArrow as StartIcon,
|
||||||
|
CheckCircle as CompleteIcon,
|
||||||
|
DoneAll as ConfirmIcon,
|
||||||
|
ArrowBack as ArrowBackIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { inventoryService } from "../services/warehouseService";
|
||||||
|
import { InventoryStatus, InventoryCountLineDto } from "../types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export default function InventoryCountPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const inventoryId = Number(id);
|
||||||
|
|
||||||
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data: inventory, isLoading } = useQuery({
|
||||||
|
queryKey: ["inventory", inventoryId],
|
||||||
|
queryFn: () => inventoryService.getById(inventoryId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const startMutation = useMutation({
|
||||||
|
mutationFn: () => inventoryService.start(inventoryId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory", inventoryId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const completeMutation = useMutation({
|
||||||
|
mutationFn: () => inventoryService.complete(inventoryId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory", inventoryId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmMutation = useMutation({
|
||||||
|
mutationFn: () => inventoryService.confirm(inventoryId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory", inventoryId] });
|
||||||
|
setConfirmDialogOpen(false);
|
||||||
|
navigate("/warehouse/inventory");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateLineMutation = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
lineId,
|
||||||
|
quantity,
|
||||||
|
}: {
|
||||||
|
lineId: number;
|
||||||
|
quantity: number;
|
||||||
|
}) => inventoryService.updateLine(lineId, quantity),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Optimistic update or invalidate
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory", inventoryId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleProcessRowUpdate = async (newRow: GridRowModel) => {
|
||||||
|
const line = newRow as InventoryCountLineDto;
|
||||||
|
if (line.countedQuantity !== undefined && line.countedQuantity !== null) {
|
||||||
|
await updateLineMutation.mutateAsync({
|
||||||
|
lineId: line.id,
|
||||||
|
quantity: Number(line.countedQuantity),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newRow;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || !inventory) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEditable = inventory.status === InventoryStatus.InProgress;
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: "articleCode", headerName: "Codice Articolo", width: 150 },
|
||||||
|
{
|
||||||
|
field: "articleDescription",
|
||||||
|
headerName: "Descrizione",
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{ field: "batchNumber", headerName: "Lotto", width: 120 },
|
||||||
|
{ field: "locationCode", headerName: "Ubicazione", width: 120 },
|
||||||
|
{
|
||||||
|
field: "theoreticalQuantity",
|
||||||
|
headerName: "Qta Teorica",
|
||||||
|
width: 120,
|
||||||
|
type: "number",
|
||||||
|
valueFormatter: (value) => (value ? Number(value).toFixed(2) : "0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "countedQuantity",
|
||||||
|
headerName: "Qta Contata",
|
||||||
|
width: 150,
|
||||||
|
type: "number",
|
||||||
|
editable: isEditable,
|
||||||
|
cellClassName: isEditable ? "editable-cell" : "",
|
||||||
|
valueFormatter: (value) =>
|
||||||
|
value !== undefined && value !== null
|
||||||
|
? Number(value).toFixed(2)
|
||||||
|
: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "difference",
|
||||||
|
headerName: "Differenza",
|
||||||
|
width: 120,
|
||||||
|
type: "number",
|
||||||
|
valueGetter: (_value, row) => {
|
||||||
|
if (
|
||||||
|
row.countedQuantity === undefined ||
|
||||||
|
row.countedQuantity === null
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
return row.countedQuantity - row.theoreticalQuantity;
|
||||||
|
},
|
||||||
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
|
if (params.value === null || params.value === undefined) return "-";
|
||||||
|
const val = Number(params.value);
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
color={val === 0 ? "text.primary" : val < 0 ? "error" : "success"}
|
||||||
|
fontWeight={val !== 0 ? "bold" : "normal"}
|
||||||
|
>
|
||||||
|
{val > 0 ? "+" : ""}
|
||||||
|
{val.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={() => navigate("/warehouse/inventory")}
|
||||||
|
>
|
||||||
|
Indietro
|
||||||
|
</Button>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Inventario: {inventory.description}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={inventory.status}
|
||||||
|
color={
|
||||||
|
inventory.status === InventoryStatus.Confirmed
|
||||||
|
? "success"
|
||||||
|
: inventory.status === InventoryStatus.InProgress
|
||||||
|
? "primary"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
|
{inventory.status === InventoryStatus.Draft && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<StartIcon />}
|
||||||
|
onClick={() => startMutation.mutate()}
|
||||||
|
>
|
||||||
|
Avvia Inventario
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{inventory.status === InventoryStatus.InProgress && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="warning"
|
||||||
|
startIcon={<CompleteIcon />}
|
||||||
|
onClick={() => completeMutation.mutate()}
|
||||||
|
>
|
||||||
|
Completa Conteggio
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{inventory.status === InventoryStatus.Completed && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
startIcon={<ConfirmIcon />}
|
||||||
|
onClick={() => setConfirmDialogOpen(true)}
|
||||||
|
>
|
||||||
|
Conferma e Rettifica
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography color="text.secondary" gutterBottom>
|
||||||
|
Data Inventario
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{dayjs(inventory.inventoryDate).format("DD/MM/YYYY")}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography color="text.secondary" gutterBottom>
|
||||||
|
Magazzino
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">{inventory.warehouseName}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography color="text.secondary" gutterBottom>
|
||||||
|
Righe Totali
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">{inventory.lineCount}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography color="text.secondary" gutterBottom>
|
||||||
|
Righe Contate
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{inventory.lines.filter((l) => l.countedQuantity !== null).length}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{inventory.status === InventoryStatus.Completed && (
|
||||||
|
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||||
|
L'inventario è completato. Verifica le differenze prima di confermare.
|
||||||
|
La conferma genererà automaticamente i movimenti di rettifica.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Paper sx={{ height: 600, width: "100%" }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={inventory.lines}
|
||||||
|
columns={columns}
|
||||||
|
processRowUpdate={handleProcessRowUpdate}
|
||||||
|
onProcessRowUpdateError={(error) => console.error(error)}
|
||||||
|
slots={{ toolbar: GridToolbar }}
|
||||||
|
slotProps={{
|
||||||
|
toolbar: {
|
||||||
|
showQuickFilter: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
sx={{
|
||||||
|
"& .editable-cell": {
|
||||||
|
backgroundColor: "#f0f8ff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={confirmDialogOpen}
|
||||||
|
onClose={() => setConfirmDialogOpen(false)}
|
||||||
|
>
|
||||||
|
<DialogTitle>Conferma Inventario</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Sei sicuro di voler confermare l'inventario? Questa operazione è
|
||||||
|
irreversibile e genererà i movimenti di rettifica per le differenze
|
||||||
|
riscontrate.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setConfirmDialogOpen(false)}>Annulla</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => confirmMutation.mutate()}
|
||||||
|
color="success"
|
||||||
|
variant="contained"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Conferma
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
258
frontend/src/modules/warehouse/pages/InventoryFormPage.tsx
Normal file
258
frontend/src/modules/warehouse/pages/InventoryFormPage.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
TextField,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
Breadcrumbs,
|
||||||
|
Link,
|
||||||
|
CircularProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Save as SaveIcon, ArrowBack as ArrowBackIcon } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
inventoryService,
|
||||||
|
warehouseLocationService,
|
||||||
|
categoryService,
|
||||||
|
} from "../services/warehouseService";
|
||||||
|
import {
|
||||||
|
CreateInventoryCountDto,
|
||||||
|
InventoryType,
|
||||||
|
} from "../types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export default function InventoryFormPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isEditing = !!id;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<CreateInventoryCountDto>({
|
||||||
|
description: "",
|
||||||
|
inventoryDate: dayjs().format("YYYY-MM-DD"),
|
||||||
|
type: InventoryType.Full,
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: warehouses = [] } = useQuery({
|
||||||
|
queryKey: ["warehouse-locations"],
|
||||||
|
queryFn: () => warehouseLocationService.getAll(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: categories = [] } = useQuery({
|
||||||
|
queryKey: ["categories"],
|
||||||
|
queryFn: () => categoryService.getAll(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: inventory, isLoading: isLoadingInventory } = useQuery({
|
||||||
|
queryKey: ["inventory", id],
|
||||||
|
queryFn: () => inventoryService.getById(Number(id)),
|
||||||
|
enabled: isEditing,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inventory) {
|
||||||
|
setFormData({
|
||||||
|
description: inventory.description,
|
||||||
|
inventoryDate: dayjs(inventory.inventoryDate).format("YYYY-MM-DD"),
|
||||||
|
warehouseId: inventory.warehouseId,
|
||||||
|
categoryId: inventory.categoryId,
|
||||||
|
type: inventory.type,
|
||||||
|
notes: inventory.notes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [inventory]);
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: (data: CreateInventoryCountDto) => inventoryService.create(data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory-counts"] });
|
||||||
|
navigate(`/warehouse/inventory/${data.id}/count`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Add update mutation if needed, currently only creation is critical
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isEditing) {
|
||||||
|
// Implement update logic if needed
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(formData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing && isLoadingInventory) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Breadcrumbs sx={{ mb: 2 }}>
|
||||||
|
<Link
|
||||||
|
color="inherit"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate("/warehouse/inventory");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Inventari
|
||||||
|
</Link>
|
||||||
|
<Typography color="text.primary">
|
||||||
|
{isEditing ? "Modifica Inventario" : "Nuovo Inventario"}
|
||||||
|
</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">
|
||||||
|
{isEditing ? `Inventario ${inventory?.code}` : "Nuovo Inventario"}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={() => navigate("/warehouse/inventory")}
|
||||||
|
>
|
||||||
|
Indietro
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper sx={{ p: 3 }}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<TextField
|
||||||
|
label="Descrizione"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, description: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<TextField
|
||||||
|
label="Data Inventario"
|
||||||
|
type="date"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
value={formData.inventoryDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, inventoryDate: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Magazzino</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.warehouseId || ""}
|
||||||
|
label="Magazzino"
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
warehouseId: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={isEditing} // Cannot change warehouse after creation usually
|
||||||
|
>
|
||||||
|
{warehouses.map((w) => (
|
||||||
|
<MenuItem key={w.id} value={w.id}>
|
||||||
|
{w.name} ({w.code})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Categoria (Opzionale)</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.categoryId || ""}
|
||||||
|
label="Categoria (Opzionale)"
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
categoryId: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>Tutte</em>
|
||||||
|
</MenuItem>
|
||||||
|
{categories.map((c) => (
|
||||||
|
<MenuItem key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Tipo Inventario</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.type}
|
||||||
|
label="Tipo Inventario"
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
type: Number(e.target.value) as InventoryType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={InventoryType.Full}>Completo</MenuItem>
|
||||||
|
<MenuItem value={InventoryType.Partial}>Parziale</MenuItem>
|
||||||
|
<MenuItem value={InventoryType.Cyclic}>Ciclico</MenuItem>
|
||||||
|
<MenuItem value={InventoryType.Sample}>A Campione</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
label="Note"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
value={formData.notes || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, notes: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }} sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
>
|
||||||
|
{isEditing ? "Salva Modifiche" : "Crea e Inizia"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
204
frontend/src/modules/warehouse/pages/InventoryListPage.tsx
Normal file
204
frontend/src/modules/warehouse/pages/InventoryListPage.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridRenderCellParams,
|
||||||
|
GridToolbar,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import {
|
||||||
|
Add as AddIcon,
|
||||||
|
Visibility as ViewIcon,
|
||||||
|
PlayArrow as StartIcon,
|
||||||
|
Cancel as CancelIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { inventoryService } from "../services/warehouseService";
|
||||||
|
import { InventoryCountDto, InventoryStatus } from "../types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export default function InventoryListPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [statusFilter] = useState<InventoryStatus | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: inventories = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["inventory-counts", statusFilter],
|
||||||
|
queryFn: () => inventoryService.getAll(statusFilter),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => inventoryService.cancel(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["inventory-counts"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
navigate("/warehouse/inventory/new");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (id: number) => {
|
||||||
|
navigate(`/warehouse/inventory/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStart = (id: number) => {
|
||||||
|
navigate(`/warehouse/inventory/${id}/count`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusChip = (status: InventoryStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case InventoryStatus.Draft:
|
||||||
|
return <Chip label="Bozza" size="small" />;
|
||||||
|
case InventoryStatus.InProgress:
|
||||||
|
return <Chip label="In Corso" color="primary" size="small" />;
|
||||||
|
case InventoryStatus.Completed:
|
||||||
|
return <Chip label="Completato" color="info" size="small" />;
|
||||||
|
case InventoryStatus.Confirmed:
|
||||||
|
return <Chip label="Confermato" color="success" size="small" />;
|
||||||
|
case InventoryStatus.Cancelled:
|
||||||
|
return <Chip label="Annullato" color="error" size="small" />;
|
||||||
|
default:
|
||||||
|
return <Chip label={status} size="small" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: "code", headerName: "Codice", width: 120 },
|
||||||
|
{ field: "description", headerName: "Descrizione", flex: 1, minWidth: 200 },
|
||||||
|
{
|
||||||
|
field: "inventoryDate",
|
||||||
|
headerName: "Data Inventario",
|
||||||
|
width: 150,
|
||||||
|
valueFormatter: (value) =>
|
||||||
|
value ? dayjs(value).format("DD/MM/YYYY") : "",
|
||||||
|
},
|
||||||
|
{ field: "warehouseName", headerName: "Magazzino", width: 180 },
|
||||||
|
{ field: "categoryName", headerName: "Categoria", width: 150 },
|
||||||
|
{
|
||||||
|
field: "status",
|
||||||
|
headerName: "Stato",
|
||||||
|
width: 120,
|
||||||
|
renderCell: (params: GridRenderCellParams<InventoryCountDto>) =>
|
||||||
|
getStatusChip(params.row.status),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "progress",
|
||||||
|
headerName: "Progresso",
|
||||||
|
width: 150,
|
||||||
|
valueGetter: (_value, row) => {
|
||||||
|
if (!row.lineCount) return "0%";
|
||||||
|
const percentage = Math.round(
|
||||||
|
(row.countedLineCount / row.lineCount) * 100
|
||||||
|
);
|
||||||
|
return `${row.countedLineCount}/${row.lineCount} (${percentage}%)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "actions",
|
||||||
|
headerName: "Azioni",
|
||||||
|
width: 180,
|
||||||
|
sortable: false,
|
||||||
|
renderCell: (params: GridRenderCellParams<InventoryCountDto>) => (
|
||||||
|
<Box>
|
||||||
|
<Tooltip title="Dettaglio">
|
||||||
|
<IconButton size="small" onClick={() => handleView(params.row.id)}>
|
||||||
|
<ViewIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{params.row.status === InventoryStatus.Draft && (
|
||||||
|
<Tooltip title="Avvia Conteggio">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleStart(params.row.id)}
|
||||||
|
>
|
||||||
|
<StartIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{params.row.status === InventoryStatus.InProgress && (
|
||||||
|
<Tooltip title="Continua Conteggio">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleStart(params.row.id)}
|
||||||
|
>
|
||||||
|
<StartIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{params.row.status === InventoryStatus.Draft && (
|
||||||
|
<Tooltip title="Annulla">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm("Sei sicuro di voler annullare questo inventario?")) {
|
||||||
|
cancelMutation.mutate(params.row.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CancelIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">Inventari Fisici</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={handleCreate}
|
||||||
|
>
|
||||||
|
Nuovo Inventario
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper sx={{ height: 600, width: "100%" }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={inventories}
|
||||||
|
columns={columns}
|
||||||
|
loading={isLoading}
|
||||||
|
slots={{ toolbar: GridToolbar }}
|
||||||
|
slotProps={{
|
||||||
|
toolbar: {
|
||||||
|
showQuickFilter: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize: 25 } },
|
||||||
|
sorting: {
|
||||||
|
sortModel: [{ field: "inventoryDate", sort: "desc" }],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -174,38 +174,30 @@ export default function WarehouseDashboard() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
{/* Actions */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mb: 4,
|
mb: 4,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "flex-end",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
gap: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Button
|
||||||
<Typography variant="h4" fontWeight="bold">
|
variant="outlined"
|
||||||
Magazzino
|
startIcon={<AddIcon />}
|
||||||
</Typography>
|
onClick={nav.goToNewInbound}
|
||||||
<Typography variant="body1" color="text.secondary">
|
>
|
||||||
Dashboard e panoramica giacenze
|
Nuovo Carico
|
||||||
</Typography>
|
</Button>
|
||||||
</Box>
|
<Button
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
variant="contained"
|
||||||
<Button
|
startIcon={<AssessmentIcon />}
|
||||||
variant="outlined"
|
onClick={nav.goToStockLevels}
|
||||||
startIcon={<AddIcon />}
|
>
|
||||||
onClick={nav.goToNewInbound}
|
Giacenze
|
||||||
>
|
</Button>
|
||||||
Nuovo Carico
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
startIcon={<AssessmentIcon />}
|
|
||||||
onClick={nav.goToStockLevels}
|
|
||||||
>
|
|
||||||
Giacenze
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@@ -313,11 +305,11 @@ export default function WarehouseDashboard() {
|
|||||||
size="small"
|
size="small"
|
||||||
color={
|
color={
|
||||||
getMovementTypeColor(movement.type) as
|
getMovementTypeColor(movement.type) as
|
||||||
| "success"
|
| "success"
|
||||||
| "error"
|
| "error"
|
||||||
| "info"
|
| "info"
|
||||||
| "warning"
|
| "warning"
|
||||||
| "default"
|
| "default"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -16,3 +16,8 @@ export { default as TransferMovementPage } from './TransferMovementPage';
|
|||||||
|
|
||||||
// Stock
|
// Stock
|
||||||
export { default as StockLevelsPage } from './StockLevelsPage';
|
export { default as StockLevelsPage } from './StockLevelsPage';
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
export { default as InventoryListPage } from './InventoryListPage';
|
||||||
|
export { default as InventoryFormPage } from './InventoryFormPage';
|
||||||
|
export { default as InventoryCountPage } from './InventoryCountPage';
|
||||||
|
|||||||
@@ -10,42 +10,55 @@ import {
|
|||||||
OutboundMovementPage,
|
OutboundMovementPage,
|
||||||
TransferMovementPage,
|
TransferMovementPage,
|
||||||
StockLevelsPage,
|
StockLevelsPage,
|
||||||
|
InventoryListPage,
|
||||||
|
InventoryFormPage,
|
||||||
|
InventoryCountPage,
|
||||||
} from "./pages";
|
} from "./pages";
|
||||||
|
|
||||||
|
import WarehouseLayout from "./components/WarehouseLayout";
|
||||||
|
|
||||||
export default function WarehouseRoutes() {
|
export default function WarehouseRoutes() {
|
||||||
return (
|
return (
|
||||||
<WarehouseProvider>
|
<WarehouseProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Dashboard */}
|
<Route element={<WarehouseLayout />}>
|
||||||
<Route index element={<WarehouseDashboard />} />
|
{/* Dashboard */}
|
||||||
|
<Route index element={<WarehouseDashboard />} />
|
||||||
|
|
||||||
{/* Articles */}
|
{/* Articles */}
|
||||||
<Route path="articles" element={<ArticlesPage />} />
|
<Route path="articles" element={<ArticlesPage />} />
|
||||||
<Route path="articles/new" element={<ArticleFormPage />} />
|
<Route path="articles/new" element={<ArticleFormPage />} />
|
||||||
<Route path="articles/:id" element={<ArticleFormPage />} />
|
<Route path="articles/:id" element={<ArticleFormPage />} />
|
||||||
<Route path="articles/:id/edit" element={<ArticleFormPage />} />
|
<Route path="articles/:id/edit" element={<ArticleFormPage />} />
|
||||||
|
|
||||||
{/* Warehouse Locations */}
|
{/* Warehouse Locations */}
|
||||||
<Route path="locations" element={<WarehouseLocationsPage />} />
|
<Route path="locations" element={<WarehouseLocationsPage />} />
|
||||||
|
|
||||||
{/* Movements */}
|
{/* Movements */}
|
||||||
<Route path="movements" element={<MovementsPage />} />
|
<Route path="movements" element={<MovementsPage />} />
|
||||||
<Route path="movements/:id" element={<MovementsPage />} />
|
<Route path="movements/:id" element={<MovementsPage />} />
|
||||||
<Route path="movements/inbound/new" element={<InboundMovementPage />} />
|
<Route path="movements/inbound/new" element={<InboundMovementPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="movements/outbound/new"
|
path="movements/outbound/new"
|
||||||
element={<OutboundMovementPage />}
|
element={<OutboundMovementPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="movements/transfer/new"
|
path="movements/transfer/new"
|
||||||
element={<TransferMovementPage />}
|
element={<TransferMovementPage />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Stock */}
|
{/* Stock */}
|
||||||
<Route path="stock" element={<StockLevelsPage />} />
|
<Route path="stock" element={<StockLevelsPage />} />
|
||||||
|
|
||||||
{/* Fallback */}
|
{/* Inventory */}
|
||||||
<Route path="*" element={<WarehouseDashboard />} />
|
<Route path="inventory" element={<InventoryListPage />} />
|
||||||
|
<Route path="inventory/new" element={<InventoryFormPage />} />
|
||||||
|
<Route path="inventory/:id" element={<InventoryFormPage />} />
|
||||||
|
<Route path="inventory/:id/count" element={<InventoryCountPage />} />
|
||||||
|
|
||||||
|
{/* Fallback */}
|
||||||
|
<Route path="*" element={<WarehouseDashboard />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</WarehouseProvider>
|
</WarehouseProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
BIN
src/Apollinare.API/apollinare.db-shm
Normal file
BIN
src/Apollinare.API/apollinare.db-shm
Normal file
Binary file not shown.
BIN
src/Apollinare.API/apollinare.db-wal
Normal file
BIN
src/Apollinare.API/apollinare.db-wal
Normal file
Binary file not shown.
Reference in New Issue
Block a user