feat: Replace warehouse product groups with hierarchical categories and update related UI and API.

This commit is contained in:
2025-12-12 13:34:52 +01:00
parent 54cf1ff276
commit 08256f0019
18 changed files with 10153 additions and 2 deletions

View File

@@ -48,8 +48,15 @@ File riassuntivo dello stato di sviluppo di Zentral.
- [2025-12-06 01:35:00 - Fix Traduzione Tab Applicazioni](./devlog/2025-12-06-013500_fix_apps_tab_translation.md) - **Completato** - [2025-12-06 01:35:00 - Fix Traduzione Tab Applicazioni](./devlog/2025-12-06-013500_fix_apps_tab_translation.md) - **Completato**
- Corretta chiave di traduzione errata per la tab "Gestione Applicazioni" e migliorata la gestione dell'aggiornamento etichette tab. - Corretta chiave di traduzione errata per la tab "Gestione Applicazioni" e migliorata la gestione dell'aggiornamento etichette tab.
- [2025-12-06 Auto Codes Reorganization](./devlog/2025-12-06-021000_autocodes_reorg.md) - **Completato** - [2025-12-06 Auto Codes Reorganization](./devlog/2025-12-06-021000_autocodes_reorg.md) - **Completato**
- [2025-12-12 Safety Training Schedule](./devlog/2025-12-12-105500_safety_training_schedule.md) - **Completato**
- [2025-12-12 Communications Module](./devlog/2025-12-12-110000_communications_module.md) - **Completato**
- [2025-12-12 Resend Integration](./devlog/2025-12-12-120000_resend_integration.md) - **Completato**
- [2025-12-12 Magazzino: Categorie Gerarchiche](./devlog/2025-12-12-133000_remove_product_groups_add_categories.md) - **Completato**
- Sostituita la logica "Gruppi Merceologici" con l'utilizzo esteso delle "Categorie Articoli" gerarchiche.
- Riorganizzazione UI Auto Codes, allineamento stile a Custom Fields, miglioramento traduzioni e categorizzazione. - Riorganizzazione UI Auto Codes, allineamento stile a Custom Fields, miglioramento traduzioni e categorizzazione.
- [2025-12-12 - Modulo Comunicazioni](./devlog/2025-12-12-110000_communications_module.md) - **In Corso** - [2025-12-12 - Modulo Comunicazioni](./devlog/2025-12-12-110000_communications_module.md) - **In Corso**
- Implementazione invio email e gestione comunicazioni. - Implementazione invio email e gestione comunicazioni.
- [2025-12-12 - Gestione Modulo Formazione (Generale)](./devlog/2025-12-12-105500_safety_training_schedule.md) - **In Corso** - [2025-12-12 - Gestione Modulo Formazione (Generale)](./devlog/2025-12-12-105500_safety_training_schedule.md) - **In Corso**
- Implementazione modulo formazione generale e scadenziario. - Implementazione modulo formazione generale e scadenziario.
- [2025-12-12 - Implementazione Gruppi Merceologici Magazzino](./devlog/2025-12-12-125000_magazzino_gruppi_merceologici.md) - **In Corso**
- Implementazione gestione gruppi merceologici per il magazzino.

View File

@@ -0,0 +1,39 @@
# Implementazione Gruppi Merceologici Magazzino
## Richiesta
Implementare la gestione dei gruppi merceologici per la categorizzazione degli articoli nel modulo magazzino, sia backend che frontend.
## Stato Attuale
- Esiste già una gestione di "Categorie Articoli" (`WarehouseArticleCategory`) che è gerarchica.
- "Gruppi Merceologici" (`WarehouseProductGroup`) sarà una nuova entità, probabilmente una classificazione parallela non gerarchica (o piatta) spesso usata per fini statistici o contabili, o semplicemente come raggruppamento alternativo.
## Piano di Lavoro
### Backend
1. **Domain Layer**
- Creare entità `WarehouseProductGroup` in `Zentral.Domain.Entities.Warehouse`.
- Campi: Code, Name, Description, IsActive.
- Aggiornare `WarehouseArticle` aggiungendo FK `ProductGroupId` e navigation property.
2. **Infrastructure Layer**
- Aggiungere `DbSet<WarehouseProductGroup>` in `ApplicationDbContext`.
- Configurare le relazioni entity framework se necessario.
- Creare Migrazione `AddWarehouseProductGroups`.
3. **Service Layer**
- Aggiornare `IWarehouseService` e `WarehouseService` con i metodi CRUD per i gruppi merceologici.
4. **API Layer**
- Creare `WarehouseProductGroupsController`.
- Aggiornare DTOs degli articoli per includere `ProductGroupId`.
### Frontend
1. **Services**
- Creare `productGroupService.ts` per chiamare le API.
2. **Pages**
- Creare `ProductGroupsPage` per elenco e gestione (CRUD).
3. **Components**
- Aggiornare il form di creazione/modifica articolo per permettere la selezione del gruppo merceologico.
4. **Routing & Navigation**
- Aggiungere rotta per `ProductGroupsPage`.
- Aggiungere voce di menu nella sidebar del magazzino.
## Note
- L'implementazione seguirà lo stile esistente del modulo Warehouse, usando Services e Controllers.

View File

@@ -0,0 +1,34 @@
# Sostituzione Gruppi Merceologici con Categorie Gerarchiche
## Stato Corrente
IMPLEMENTATO
## Descrizione
Sostituita la gestione separata dei "Gruppi Merceologici" con l'utilizzo potenziato delle Categorie Articoli (`WarehouseArticleCategory`) già esistenti e gerarchiche.
## Modifiche Apportate
### Backend
- **Revert**: Rimossa entity `WarehouseProductGroup` e relativi controller e service.
- **Migration**: Creata e applicata migrazione `RemoveWarehouseProductGroups` per rimuovere la tabella dal database.
- **Services**: `WarehouseService` ripulito da logica `ProductGroups`.
### Frontend
- **Revert**: Rimossa pagina `ProductGroupsPage` e riferimenti nel codice.
- **New Feature**: Creata pagina `CategoriesPage` (`/warehouse/categories`) per gestire le categorie in modalità albero.
- Create
- Update
- Delete
- Struttura gerarchica visualizzata (Tree View).
- **Article Form**: Rimossa selezione "Gruppo Merceologico". La selezione della categoria utilizza `CategoryTree` appiattito per la selezione.
- **Navigation**: Aggiunto link "Categorie" nella sidebar del Magazzino.
## Note Tecniche
- La gestione delle categorie sfrutta la ricorsività supportata dall'entity `WarehouseArticleCategory`.
- L'interfaccia utente permette di gestire la gerarchia creando categorie "root" o sottocategorie.
## Verifica
- **Backend API**:
- `GET /api/warehouse/categories` -> Disponibile.
- `GET /api/warehouse/categories/tree` -> Disponibile (ritorna JSON corretto).
- `GET /api/warehouse/product-groups` -> **404 Not Found** (Correttamente rimosso).

View File

@@ -28,6 +28,10 @@ public interface IWarehouseService
Task<WarehouseArticleCategory> UpdateCategoryAsync(WarehouseArticleCategory category); Task<WarehouseArticleCategory> UpdateCategoryAsync(WarehouseArticleCategory category);
Task DeleteCategoryAsync(int id); Task DeleteCategoryAsync(int id);
// ===============================================
// GRUPPI MERCEOLOGICI
// ===============================================
// =============================================== // ===============================================
// MAGAZZINI // MAGAZZINI
// =============================================== // ===============================================

View File

@@ -60,6 +60,7 @@ public class WarehouseService : IWarehouseService
if (filter.CategoryId.HasValue) if (filter.CategoryId.HasValue)
query = query.Where(a => a.CategoryId == filter.CategoryId); query = query.Where(a => a.CategoryId == filter.CategoryId);
if (filter.IsActive.HasValue) if (filter.IsActive.HasValue)
query = query.Where(a => a.IsActive == filter.IsActive); query = query.Where(a => a.IsActive == filter.IsActive);
@@ -336,6 +337,7 @@ public class WarehouseService : IWarehouseService
#endregion #endregion
#region Magazzini #region Magazzini
public async Task<List<WarehouseLocation>> GetWarehousesAsync(bool includeInactive = false) public async Task<List<WarehouseLocation>> GetWarehousesAsync(bool includeInactive = false)

View File

@@ -40,6 +40,11 @@ public class WarehouseArticle : BaseEntity
/// </summary> /// </summary>
public int? CategoryId { get; set; } public int? CategoryId { get; set; }
/// <summary>
/// Gruppo merceologico
/// </summary>
public int? ProductGroupId { get; set; }
/// <summary> /// <summary>
/// Unità di misura principale (es. PZ, KG, LT, MT) /// Unità di misura principale (es. PZ, KG, LT, MT)
/// </summary> /// </summary>

View File

@@ -445,6 +445,7 @@ public class ZentralDbContext : DbContext
.WithMany(c => c.Articles) .WithMany(c => c.Articles)
.HasForeignKey(e => e.CategoryId) .HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
}); });
// ArticleBatch // ArticleBatch

View File

@@ -0,0 +1,86 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Zentral.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddWarehouseProductGroups : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ProductGroupId",
table: "WarehouseArticles",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "WarehouseProductGroups",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Code = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
Notes = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WarehouseProductGroups", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_WarehouseArticles_ProductGroupId",
table: "WarehouseArticles",
column: "ProductGroupId");
migrationBuilder.CreateIndex(
name: "IX_WarehouseProductGroups_Code",
table: "WarehouseProductGroups",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WarehouseProductGroups_IsActive",
table: "WarehouseProductGroups",
column: "IsActive");
migrationBuilder.AddForeignKey(
name: "FK_WarehouseArticles_WarehouseProductGroups_ProductGroupId",
table: "WarehouseArticles",
column: "ProductGroupId",
principalTable: "WarehouseProductGroups",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_WarehouseArticles_WarehouseProductGroups_ProductGroupId",
table: "WarehouseArticles");
migrationBuilder.DropTable(
name: "WarehouseProductGroups");
migrationBuilder.DropIndex(
name: "IX_WarehouseArticles_ProductGroupId",
table: "WarehouseArticles");
migrationBuilder.DropColumn(
name: "ProductGroupId",
table: "WarehouseArticles");
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Zentral.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class RemoveWarehouseProductGroups : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_WarehouseArticles_WarehouseProductGroups_ProductGroupId",
table: "WarehouseArticles");
migrationBuilder.DropTable(
name: "WarehouseProductGroups");
migrationBuilder.DropIndex(
name: "IX_WarehouseArticles_ProductGroupId",
table: "WarehouseArticles");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WarehouseProductGroups",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Code = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
CustomFieldsJson = table.Column<string>(type: "TEXT", nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Notes = table.Column<string>(type: "TEXT", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WarehouseProductGroups", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_WarehouseArticles_ProductGroupId",
table: "WarehouseArticles",
column: "ProductGroupId");
migrationBuilder.CreateIndex(
name: "IX_WarehouseProductGroups_Code",
table: "WarehouseProductGroups",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WarehouseProductGroups_IsActive",
table: "WarehouseProductGroups",
column: "IsActive");
migrationBuilder.AddForeignKey(
name: "FK_WarehouseArticles_WarehouseProductGroups_ProductGroupId",
table: "WarehouseArticles",
column: "ProductGroupId",
principalTable: "WarehouseProductGroups",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

View File

@@ -3675,6 +3675,9 @@ namespace Zentral.Infrastructure.Migrations
b.Property<string>("Notes") b.Property<string>("Notes")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("ProductGroupId")
.HasColumnType("INTEGER");
b.Property<decimal?>("ReorderPoint") b.Property<decimal?>("ReorderPoint")
.HasPrecision(18, 4) .HasPrecision(18, 4)
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@@ -0,0 +1,304 @@
import React, { useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
Switch,
FormControlLabel,
Collapse,
List,
ListItem,
ListItemText,
ListItemIcon,
Paper,
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
Folder as FolderIcon,
ExpandMore as ExpandMoreIcon,
KeyboardArrowRight as KeyboardArrowRightIcon,
} from '@mui/icons-material';
import { useCategoryTree, useCreateCategory, useUpdateCategory, useDeleteCategory } from '../hooks';
import { CategoryTreeDto, CreateCategoryDto, UpdateCategoryDto } from '../types';
interface CategoryItemProps {
category: CategoryTreeDto;
onEdit: (category: CategoryTreeDto) => void;
onDelete: (id: number) => void;
onAddSubCategory: (parentId: number) => void;
}
const CategoryItem: React.FC<CategoryItemProps> = ({ category, onEdit, onDelete, onAddSubCategory }) => {
const [open, setOpen] = useState(true);
const hasChildren = category.children && category.children.length > 0;
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
setOpen(!open);
};
return (
<>
<ListItem
sx={{
pl: category.level * 4,
borderBottom: '1px solid #eee',
'&:hover': { bgcolor: 'action.hover' },
}}
secondaryAction={
<Box>
<IconButton size="small" onClick={() => onAddSubCategory(category.id)} title="Aggiungi sottocategoria">
<AddIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => onEdit(category)} title="Modifica">
<EditIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => onDelete(category.id)} title="Elimina" color="error">
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
}
>
<ListItemIcon sx={{ minWidth: 40, cursor: hasChildren ? 'pointer' : 'default' }} onClick={(e) => hasChildren && handleToggle(e)}>
{hasChildren ? (open ? <ExpandMoreIcon /> : <KeyboardArrowRightIcon />) : <Box sx={{ width: 24 }} />}
</ListItemIcon>
<ListItemIcon>
<FolderIcon color={category.isActive ? 'primary' : 'disabled'} />
</ListItemIcon>
<ListItemText
primary={
<Typography variant="body1" fontWeight="medium">
{category.name}
</Typography>
}
secondary={category.description}
/>
</ListItem>
{hasChildren && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{category.children.map((child) => (
<CategoryItem
key={child.id}
category={child}
onEdit={onEdit}
onDelete={onDelete}
onAddSubCategory={onAddSubCategory}
/>
))}
</List>
</Collapse>
)}
</>
);
};
export default function CategoriesPage() {
const { data: categories, isLoading } = useCategoryTree();
const createMutation = useCreateCategory();
const updateMutation = useUpdateCategory();
const deleteMutation = useDeleteCategory();
const [openDialog, setOpenDialog] = useState(false);
const [editingCategory, setEditingCategory] = useState<CategoryTreeDto | null>(null);
const [parentCategoryId, setParentCategoryId] = useState<number | undefined>(undefined);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [categoryToDelete, setCategoryToDelete] = useState<number | null>(null);
const [formData, setFormData] = useState<CreateCategoryDto>({
name: '',
description: '',
sortOrder: 0,
parentCategoryId: undefined,
});
const [isActive, setIsActive] = useState(true);
const handleOpenDialog = (category?: CategoryTreeDto, parentId?: number) => {
if (category) {
setEditingCategory(category);
setFormData({
name: category.name,
description: category.description || '',
sortOrder: 0, // Not in TreeDto usually, default to 0
parentCategoryId: undefined, // Usually handled by structure
});
setIsActive(category.isActive);
setParentCategoryId(undefined);
} else {
setEditingCategory(null);
setFormData({
name: '',
description: '',
sortOrder: 0,
parentCategoryId: parentId,
});
setIsActive(true);
setParentCategoryId(parentId);
}
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
setEditingCategory(null);
setParentCategoryId(undefined);
};
const handleSubmit = async () => {
try {
if (editingCategory) {
const updateData: UpdateCategoryDto = {
name: formData.name,
description: formData.description || undefined,
sortOrder: formData.sortOrder,
isActive: isActive,
};
await updateMutation.mutateAsync({ id: editingCategory.id, data: updateData });
} else {
const createData: CreateCategoryDto = {
name: formData.name,
description: formData.description || undefined,
sortOrder: formData.sortOrder,
parentCategoryId: parentCategoryId,
};
await createMutation.mutateAsync(createData);
}
handleCloseDialog();
} catch (error) {
console.error("Error saving category:", error);
}
};
const handleDeleteClick = (id: number) => {
setCategoryToDelete(id);
setDeleteConfirmOpen(true);
};
const handleConfirmDelete = async () => {
if (categoryToDelete) {
try {
await deleteMutation.mutateAsync(categoryToDelete);
setDeleteConfirmOpen(false);
setCategoryToDelete(null);
} catch (error) {
console.error("Error deleting category:", error);
}
}
};
if (isLoading) {
return <Typography>Caricamento...</Typography>;
}
return (
<Box>
<Box sx={{ mb: 3, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h5" fontWeight="bold">
Categorie Articoli
</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenDialog()}
>
Nuova Categoria Root
</Button>
</Box>
<Paper elevation={0} variant="outlined">
<List>
{categories?.map((category) => (
<CategoryItem
key={category.id}
category={category}
onEdit={handleOpenDialog}
onDelete={handleDeleteClick}
onAddSubCategory={(parentId) => handleOpenDialog(undefined, parentId)}
/>
))}
{(!categories || categories.length === 0) && (
<ListItem>
<ListItemText primary="Nessuna categoria trovata" />
</ListItem>
)}
</List>
</Paper>
{/* Create/Edit Dialog */}
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{editingCategory ? "Modifica Categoria" : "Nuova Categoria"}
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<TextField
label="Nome"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
fullWidth
required
/>
<TextField
label="Descrizione"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
fullWidth
multiline
rows={3}
/>
<TextField
label="Ordinamento"
type="number"
value={formData.sortOrder}
onChange={(e) => setFormData({ ...formData, sortOrder: parseInt(e.target.value) || 0 })}
fullWidth
/>
{editingCategory && (
<FormControlLabel
control={
<Switch
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
}
label="Attivo"
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Annulla</Button>
<Button onClick={handleSubmit} variant="contained" disabled={!formData.name}>
Salva
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteConfirmOpen} onClose={() => setDeleteConfirmOpen(false)}>
<DialogTitle>Conferma Eliminazione</DialogTitle>
<DialogContent>
<Typography>
Sei sicuro di voler eliminare questa categoria? L'operazione non può essere annullata.
Se la categoria contiene sottocategorie o articoli, potrebbe non essere possibile eliminarla.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)}>Annulla</Button>
<Button onClick={handleConfirmDelete} color="error" variant="contained">
Elimina
</Button>
</DialogActions>
</Dialog>
</Box>
);
}

View File

@@ -4,6 +4,7 @@ export { default as WarehouseDashboard } from './WarehouseDashboard';
// Articles // Articles
export { default as ArticlesPage } from './ArticlesPage'; export { default as ArticlesPage } from './ArticlesPage';
export { default as ArticleFormPage } from './ArticleFormPage'; export { default as ArticleFormPage } from './ArticleFormPage';
export { default as CategoriesPage } from './CategoriesPage';
// Warehouse Locations // Warehouse Locations
export { default as WarehouseLocationsPage } from './WarehouseLocationsPage'; export { default as WarehouseLocationsPage } from './WarehouseLocationsPage';

View File

@@ -4,6 +4,7 @@ import {
WarehouseDashboard, WarehouseDashboard,
ArticlesPage, ArticlesPage,
ArticleFormPage, ArticleFormPage,
CategoriesPage,
WarehouseLocationsPage, WarehouseLocationsPage,
MovementsPage, MovementsPage,
InboundMovementPage, InboundMovementPage,
@@ -30,6 +31,7 @@ export default function WarehouseRoutes() {
<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 />} />
<Route path="categories" element={<CategoriesPage />} />
{/* Warehouse Locations */} {/* Warehouse Locations */}
<Route path="locations" element={<WarehouseLocationsPage />} /> <Route path="locations" element={<WarehouseLocationsPage />} />

View File

@@ -37,7 +37,6 @@ import {
UpdateArticleDto, UpdateArticleDto,
UpdateBatchDto, UpdateBatchDto,
UpdateCategoryDto, UpdateCategoryDto,
UpdateWarehouseDto,
ValuationMethod, ValuationMethod,
WarehouseLocationDto, WarehouseLocationDto,
} from "../types"; } from "../types";
@@ -558,6 +557,7 @@ export const inventoryService = {
}, },
}; };
// Export all services // Export all services
export default { export default {
locations: warehouseLocationService, locations: warehouseLocationService,

View File

@@ -230,6 +230,7 @@ export interface UpdateCategoryDto {
notes?: string; notes?: string;
} }
// =============================================== // ===============================================
// ARTICLE // ARTICLE
// =============================================== // ===============================================

View File

@@ -36,6 +36,7 @@ import {
Timeline as TimelineIcon, Timeline as TimelineIcon,
PrecisionManufacturing as ManufacturingIcon, PrecisionManufacturing as ManufacturingIcon,
Category as CategoryIcon, Category as CategoryIcon,
Folder as FolderIcon,
AttachMoney as AttachMoneyIcon, AttachMoney as AttachMoneyIcon,
Receipt as ReceiptIcon, Receipt as ReceiptIcon,
ChevronLeft, ChevronLeft,
@@ -116,6 +117,7 @@ export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse
children: [ children: [
{ id: 'wh-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse', translationKey: 'menu.warehouse' }, { id: 'wh-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse', translationKey: 'menu.warehouse' },
{ id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles', translationKey: 'menu.articles' }, { id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles', translationKey: 'menu.articles' },
{ id: 'wh-categories', label: t('menu.categories'), icon: <FolderIcon />, path: '/warehouse/categories', translationKey: 'menu.categories' },
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations', translationKey: 'menu.location' }, { id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations', translationKey: 'menu.location' },
{ id: 'wh-movements', label: t('menu.movements'), icon: <SwapIcon />, path: '/warehouse/movements', translationKey: 'menu.movements' }, { id: 'wh-movements', label: t('menu.movements'), icon: <SwapIcon />, path: '/warehouse/movements', translationKey: 'menu.movements' },
{ id: 'wh-stock', label: t('menu.stock'), icon: <StorageIcon />, path: '/warehouse/stock', translationKey: 'menu.stock' }, { id: 'wh-stock', label: t('menu.stock'), icon: <StorageIcon />, path: '/warehouse/stock', translationKey: 'menu.stock' },