feat: Replace warehouse product groups with hierarchical categories and update related UI and API.
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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
|
||||||
// ===============================================
|
// ===============================================
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
4823
src/backend/Zentral.Infrastructure/Migrations/20251212115332_AddWarehouseProductGroups.Designer.cs
generated
Normal file
4823
src/backend/Zentral.Infrastructure/Migrations/20251212115332_AddWarehouseProductGroups.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4761
src/backend/Zentral.Infrastructure/Migrations/20251212122107_RemoveWarehouseProductGroups.Designer.cs
generated
Normal file
4761
src/backend/Zentral.Infrastructure/Migrations/20251212122107_RemoveWarehouseProductGroups.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
304
src/frontend/src/apps/warehouse/pages/CategoriesPage.tsx
Normal file
304
src/frontend/src/apps/warehouse/pages/CategoriesPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ export interface UpdateCategoryDto {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===============================================
|
// ===============================================
|
||||||
// ARTICLE
|
// ARTICLE
|
||||||
// ===============================================
|
// ===============================================
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user