-
This commit is contained in:
182
CLAUDE.md
182
CLAUDE.md
@@ -46,12 +46,103 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
||||
|
||||
## Quick Start - Session Recovery
|
||||
|
||||
**Ultima sessione:** 29 Novembre 2025 (sera)
|
||||
**Ultima sessione:** 30 Novembre 2025
|
||||
|
||||
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
|
||||
|
||||
**Lavoro completato nell'ultima sessione:**
|
||||
|
||||
- **NUOVA FEATURE: Sistema Codici Automatici Configurabili** - COMPLETATO
|
||||
- **Obiettivo:** Sistema admin per configurare la generazione automatica di codici (articoli, magazzini, movimenti, ecc.)
|
||||
- **Backend implementato:**
|
||||
- `AutoCode.cs` - Entity con pattern configurabile, prefisso, sequenza, reset periodico
|
||||
- `AutoCodeService.cs` - Logica business (generazione, preview, reset, validazione pattern)
|
||||
- `AutoCodesController.cs` - API REST complete
|
||||
- Migration EF Core `AddAutoCodeSystem`
|
||||
- Seed automatico configurazioni default per tutte le entità
|
||||
- **Frontend implementato:**
|
||||
- `autoCode.ts` - Types TypeScript
|
||||
- `autoCodeService.ts` - API calls
|
||||
- `AutoCodesAdminPage.tsx` - Pagina admin con tabella configurazioni, dialog modifica, guida pattern
|
||||
- **Pattern supportati:**
|
||||
- `{PREFIX}` - Prefisso configurabile
|
||||
- `{SEQ:n}` - Sequenza numerica con n cifre
|
||||
- `{YYYY}`, `{YY}` - Anno
|
||||
- `{MM}`, `{DD}` - Mese, Giorno
|
||||
- **Funzionalità:**
|
||||
- Configurazione per entità (warehouse_article, stock_movement, cliente, evento, ecc.)
|
||||
- Reset sequenza annuale o mensile automatico
|
||||
- Preview prossimo codice senza incremento
|
||||
- Reset manuale sequenza
|
||||
- Abilitazione/disabilitazione per entità
|
||||
- Raggruppamento per modulo nell'UI
|
||||
- **API Endpoints:**
|
||||
- `GET /api/autocodes` - Lista configurazioni
|
||||
- `GET /api/autocodes/{entityCode}` - Dettaglio
|
||||
- `GET /api/autocodes/{entityCode}/preview` - Anteprima prossimo codice
|
||||
- `POST /api/autocodes/{entityCode}/generate` - Genera nuovo codice
|
||||
- `PUT /api/autocodes/{id}` - Aggiorna configurazione
|
||||
- `POST /api/autocodes/{entityCode}/reset-sequence` - Reset sequenza
|
||||
- `GET /api/autocodes/placeholders` - Lista placeholder disponibili
|
||||
- **File principali:**
|
||||
- `src/Apollinare.Domain/Entities/AutoCode.cs`
|
||||
- `src/Apollinare.API/Services/AutoCodeService.cs`
|
||||
- `src/Apollinare.API/Controllers/AutoCodesController.cs`
|
||||
- `frontend/src/pages/AutoCodesAdminPage.tsx`
|
||||
|
||||
**Lavoro completato nelle sessioni precedenti (29 Novembre 2025 notte):**
|
||||
|
||||
- **NUOVA FEATURE: Modulo Magazzino (warehouse)** - COMPLETATO
|
||||
- **Backend implementato:**
|
||||
- Entities complete in `/src/Apollinare.Domain/Entities/Warehouse/`:
|
||||
- `WarehouseLocation.cs` - Magazzini fisici/logici con Type enum (Physical, Virtual, Transit)
|
||||
- `WarehouseArticleCategory.cs` - Categorie gerarchiche con Color, Icon, Level, FullPath
|
||||
- `WarehouseArticle.cs` - Articoli con batch/serial management flags, valorizzazione
|
||||
- `ArticleBatch.cs` - Tracciabilità lotti con scadenza
|
||||
- `ArticleSerial.cs` - Tracciabilità numeri seriali
|
||||
- `StockLevel.cs` - Giacenze per articolo/magazzino/batch
|
||||
- `StockMovement.cs` - Movimenti (Inbound/Outbound/Transfer/Adjustment)
|
||||
- `StockMovementLine.cs` - Righe movimento
|
||||
- `MovementReason.cs` - Causali movimento
|
||||
- `ArticleBarcode.cs` - Multi-barcode support
|
||||
- `StockValuation.cs` + `StockValuationLayer.cs` - Valorizzazione periodo e layer FIFO/LIFO
|
||||
- `InventoryCount.cs` + `InventoryCountLine.cs` - Inventari fisici
|
||||
- Service completo `WarehouseService.cs` con:
|
||||
- CRUD articoli, categorie, magazzini
|
||||
- Gestione movimenti (carico/scarico/trasferimento/rettifica)
|
||||
- Conferma movimenti con aggiornamento giacenze
|
||||
- Calcolo valorizzazione (WeightedAverage, FIFO, LIFO, StandardCost)
|
||||
- Gestione partite e seriali
|
||||
- Controllers REST in `/src/Apollinare.API/Modules/Warehouse/Controllers/`:
|
||||
- `WarehouseLocationsController.cs`
|
||||
- `WarehouseArticlesController.cs`
|
||||
- `WarehouseArticleCategoriesController.cs`
|
||||
- `StockMovementsController.cs`
|
||||
- `StockLevelsController.cs`
|
||||
- Seed dati default (magazzino principale + transito, categorie base, causali)
|
||||
|
||||
- **CONFIGURAZIONE: EF Core Code First Migrations** - COMPLETATO
|
||||
- **Problema:** Le tabelle venivano create manualmente invece che con migrations EF Core
|
||||
- **Soluzione implementata:**
|
||||
- Sostituito `db.Database.EnsureCreated()` con `db.Database.MigrateAsync()` in `Program.cs`
|
||||
- Creata migration `InitialCreate` con tutte le tabelle (sistema + moduli + warehouse)
|
||||
- Le migrations vengono applicate **automaticamente all'avvio** dell'applicazione
|
||||
- Logging delle migrations pendenti prima dell'applicazione
|
||||
- **Comandi per future migrations:**
|
||||
|
||||
```bash
|
||||
# Creare nuova migration
|
||||
dotnet ef migrations add NomeMigration \
|
||||
--project src/Apollinare.Infrastructure \
|
||||
--startup-project src/Apollinare.API
|
||||
|
||||
# L'applicazione è AUTOMATICA all'avvio - non serve "dotnet ef database update"
|
||||
```
|
||||
|
||||
- **File modificati:** `Program.cs`, `src/Apollinare.Infrastructure/Migrations/`
|
||||
|
||||
**Lavoro completato nelle sessioni precedenti (29 Novembre 2025 sera):**
|
||||
|
||||
- **NUOVA FEATURE: Sistema Moduli Applicativi** - COMPLETATO (continuazione)
|
||||
- **Obiettivo:** Sistema di modularizzazione per gestire licenze, abbonamenti e funzionalità dinamiche
|
||||
- **Backend implementato:**
|
||||
@@ -318,11 +409,14 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
||||
|
||||
**MODULI BUSINESS (PRIORITÀ ALTA):**
|
||||
|
||||
1. [ ] **Implementare modulo Magazzino (warehouse)** - Base per tutti gli altri
|
||||
2. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
|
||||
3. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
|
||||
4. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
|
||||
5. [ ] **Implementare modulo Qualità (quality)** - Indipendente
|
||||
1. [x] **Implementare modulo Magazzino (warehouse)** - COMPLETATO (backend)
|
||||
- Backend: Entities, Service, Controllers, API completi
|
||||
- Manca: Frontend (pagine React per gestione articoli, movimenti, giacenze)
|
||||
2. [ ] **Frontend modulo Magazzino** - Pagine React per warehouse
|
||||
3. [ ] **Implementare modulo Acquisti (purchases)** - Dipende da Magazzino
|
||||
4. [ ] **Implementare modulo Vendite (sales)** - Dipende da Magazzino
|
||||
5. [ ] **Implementare modulo Produzione (production)** - Dipende da Magazzino
|
||||
6. [ ] **Implementare modulo Qualità (quality)** - Indipendente
|
||||
|
||||
**Report System (completamento):**
|
||||
|
||||
@@ -1855,3 +1949,79 @@ public interface IWarehouseService
|
||||
- `ModulePurchasePage.tsx`: Rimosso `moduleService` import
|
||||
- `ModulesAdminPage.tsx`: Rimosso `PowerIcon`, `CheckIcon`, `CancelIcon`
|
||||
- **File:** Vari componenti frontend
|
||||
|
||||
33. **EF Core Code First vs Database First (FIX 29/11/2025):**
|
||||
- **Problema:** Le tabelle venivano create manualmente con SQL invece di usare EF Core migrations
|
||||
- **Causa:** `db.Database.EnsureCreated()` non supporta migrations e crea le tabelle direttamente
|
||||
- **Soluzione:**
|
||||
- Sostituito `EnsureCreated()` con `MigrateAsync()` in `Program.cs`
|
||||
- Rimosso database e migrations esistenti
|
||||
- Creata nuova migration `InitialCreate` con `dotnet ef migrations add`
|
||||
- Le migrations vengono ora applicate automaticamente all'avvio
|
||||
- **File:** `Program.cs`, `src/Apollinare.Infrastructure/Migrations/20251129134709_InitialCreate.cs`
|
||||
|
||||
34. **Modulo Warehouse - Struttura Completa (IMPLEMENTATO 29/11/2025):**
|
||||
- **Entities in `/src/Apollinare.Domain/Entities/Warehouse/`:**
|
||||
- `WarehouseLocation.cs` - Magazzini (Physical, Virtual, Transit)
|
||||
- `WarehouseArticle.cs` - Articoli con batch/serial flags
|
||||
- `WarehouseArticleCategory.cs` - Categorie gerarchiche
|
||||
- `ArticleBatch.cs` - Lotti con scadenza
|
||||
- `ArticleSerial.cs` - Numeri seriali
|
||||
- `StockLevel.cs` - Giacenze
|
||||
- `StockMovement.cs` + `StockMovementLine.cs` - Movimenti
|
||||
- `MovementReason.cs` - Causali
|
||||
- `ArticleBarcode.cs` - Multi-barcode
|
||||
- `StockValuation.cs` + `StockValuationLayer.cs` - Valorizzazione
|
||||
- `InventoryCount.cs` + `InventoryCountLine.cs` - Inventari
|
||||
- **Service:** `WarehouseService.cs` con CRUD completo, movimenti, giacenze, valorizzazione
|
||||
- **Controllers:** `WarehouseLocationsController`, `WarehouseArticlesController`, `WarehouseArticleCategoriesController`, `StockMovementsController`, `StockLevelsController`
|
||||
- **API Endpoints principali:**
|
||||
- `GET/POST /api/warehouse/locations` - Magazzini
|
||||
- `GET/POST /api/warehouse/articles` - Articoli
|
||||
- `GET/POST /api/warehouse/categories` - Categorie
|
||||
- `POST /api/warehouse/movements/inbound` - Carichi
|
||||
- `POST /api/warehouse/movements/outbound` - Scarichi
|
||||
- `POST /api/warehouse/movements/{id}/confirm` - Conferma movimento
|
||||
- `GET /api/warehouse/articles/{id}/stock` - Giacenza articolo
|
||||
|
||||
35. **Sistema Codici Automatici Configurabili (IMPLEMENTATO 30/11/2025):**
|
||||
- **Obiettivo:** Sistema per generare automaticamente codici univoci per tutte le entità (articoli, magazzini, movimenti, clienti, eventi, ecc.)
|
||||
- **Entity:** `AutoCode.cs` in `/src/Apollinare.Domain/Entities/`
|
||||
- `EntityCode` - Identificativo entità (es. "warehouse_article")
|
||||
- `EntityName` - Nome visualizzato
|
||||
- `Prefix` - Prefisso per {PREFIX}
|
||||
- `Pattern` - Pattern con placeholder (es. "{PREFIX}{YYYY}-{SEQ:5}")
|
||||
- `LastSequence` - Ultimo numero usato
|
||||
- `ResetSequenceYearly` / `ResetSequenceMonthly` - Reset automatico
|
||||
- `IsEnabled` - Abilita generazione
|
||||
- `IsReadOnly` - Codice non modificabile
|
||||
- `ModuleCode` - Raggruppa per modulo
|
||||
- **Service:** `AutoCodeService.cs` in `/src/Apollinare.API/Services/`
|
||||
- `GenerateNextCodeAsync(entityCode)` - Genera e incrementa
|
||||
- `PreviewNextCodeAsync(entityCode)` - Anteprima senza incremento
|
||||
- `IsCodeUniqueAsync(entityCode, code)` - Verifica univocità
|
||||
- `ResetSequenceAsync(entityCode)` - Reset manuale
|
||||
- `SeedDefaultConfigurationsAsync()` - Seed configurazioni default
|
||||
- **Controller:** `AutoCodesController.cs`
|
||||
- **Frontend:**
|
||||
- `AutoCodesAdminPage.tsx` - Pagina admin con accordions per modulo
|
||||
- `autoCodeService.ts` - API calls
|
||||
- `autoCode.ts` - Types
|
||||
- **Pattern supportati:**
|
||||
- `{PREFIX}` - Prefisso configurabile
|
||||
- `{SEQ:n}` - Sequenza con n cifre (es. {SEQ:5} → 00001)
|
||||
- `{YYYY}`, `{YY}` - Anno 4 o 2 cifre
|
||||
- `{MM}`, `{DD}` - Mese e giorno
|
||||
- Testo statico (es. "-", "/")
|
||||
- **Entità preconfigurate:**
|
||||
- Core: cliente, evento, articolo
|
||||
- Warehouse: warehouse_location, warehouse_article, warehouse_category, stock_movement, inventory_count, article_batch
|
||||
- Purchases (future): purchase_order, supplier
|
||||
- Sales (future): sales_order, invoice
|
||||
- **Esempio utilizzo nel codice:**
|
||||
```csharp
|
||||
// Nel service che crea un articolo
|
||||
var code = await _autoCodeService.GenerateNextCodeAsync("warehouse_article");
|
||||
if (code != null)
|
||||
article.Code = code;
|
||||
```
|
||||
|
||||
@@ -19,6 +19,7 @@ import ReportTemplatesPage from "./pages/ReportTemplatesPage";
|
||||
import ReportEditorPage from "./pages/ReportEditorPage";
|
||||
import ModulesAdminPage from "./pages/ModulesAdminPage";
|
||||
import ModulePurchasePage from "./pages/ModulePurchasePage";
|
||||
import AutoCodesAdminPage from "./pages/AutoCodesAdminPage";
|
||||
import WarehouseRoutes from "./modules/warehouse/routes";
|
||||
import { ModuleGuard } from "./components/ModuleGuard";
|
||||
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||
@@ -90,12 +91,16 @@ function App() {
|
||||
path="report-editor/:id"
|
||||
element={<ReportEditorPage />}
|
||||
/>
|
||||
{/* Moduli */}
|
||||
{/* Admin */}
|
||||
<Route path="modules" element={<ModulesAdminPage />} />
|
||||
<Route
|
||||
path="modules/purchase/:code"
|
||||
element={<ModulePurchasePage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/auto-codes"
|
||||
element={<AutoCodesAdminPage />}
|
||||
/>
|
||||
{/* Warehouse Module */}
|
||||
<Route
|
||||
path="warehouse/*"
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Close as CloseIcon,
|
||||
Extension as ModulesIcon,
|
||||
Warehouse as WarehouseIcon,
|
||||
Code as AutoCodeIcon,
|
||||
} from "@mui/icons-material";
|
||||
import CollaborationIndicator from "./collaboration/CollaborationIndicator";
|
||||
import { useModules } from "../contexts/ModuleContext";
|
||||
@@ -52,6 +53,7 @@ const menuItems = [
|
||||
},
|
||||
{ text: "Report", icon: <PrintIcon />, path: "/report-templates" },
|
||||
{ text: "Moduli", icon: <ModulesIcon />, path: "/modules" },
|
||||
{ text: "Codici Auto", icon: <AutoCodeIcon />, path: "/admin/auto-codes" },
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
|
||||
@@ -86,6 +86,7 @@ export default function ArticleFormPage() {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [formData, setFormData] = useState({
|
||||
code: "",
|
||||
alternativeCode: "",
|
||||
description: "",
|
||||
shortDescription: "",
|
||||
categoryId: undefined as number | undefined,
|
||||
@@ -138,6 +139,7 @@ export default function ArticleFormPage() {
|
||||
if (article) {
|
||||
setFormData({
|
||||
code: article.code,
|
||||
alternativeCode: article.alternativeCode || "",
|
||||
description: article.description,
|
||||
shortDescription: article.shortDescription || "",
|
||||
categoryId: article.categoryId,
|
||||
@@ -190,7 +192,8 @@ export default function ArticleFormPage() {
|
||||
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!formData.code.trim()) {
|
||||
// Il codice è generato automaticamente, non richiede validazione in creazione
|
||||
if (!isNew && !formData.code.trim()) {
|
||||
newErrors.code = "Il codice è obbligatorio";
|
||||
}
|
||||
if (!formData.description.trim()) {
|
||||
@@ -211,9 +214,10 @@ export default function ArticleFormPage() {
|
||||
let savedId: number;
|
||||
if (isNew) {
|
||||
const createData: CreateArticleDto = {
|
||||
code: formData.code,
|
||||
// code è generato automaticamente dal backend
|
||||
description: formData.description,
|
||||
shortDescription: formData.shortDescription || undefined,
|
||||
alternativeCode: formData.alternativeCode || undefined,
|
||||
categoryId: formData.categoryId,
|
||||
unitOfMeasure: formData.unitOfMeasure,
|
||||
barcode: formData.barcode || undefined,
|
||||
@@ -234,9 +238,10 @@ export default function ArticleFormPage() {
|
||||
savedId = result.id;
|
||||
} else {
|
||||
const updateData: UpdateArticleDto = {
|
||||
code: formData.code,
|
||||
// code non modificabile
|
||||
description: formData.description,
|
||||
shortDescription: formData.shortDescription || undefined,
|
||||
alternativeCode: formData.alternativeCode || undefined,
|
||||
categoryId: formData.categoryId,
|
||||
unitOfMeasure: formData.unitOfMeasure,
|
||||
barcode: formData.barcode || undefined,
|
||||
@@ -327,19 +332,46 @@ export default function ArticleFormPage() {
|
||||
Informazioni Base
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice"
|
||||
value={formData.code}
|
||||
onChange={(e) => handleChange("code", e.target.value)}
|
||||
error={!!errors.code}
|
||||
helperText={errors.code}
|
||||
required
|
||||
disabled={!isNew}
|
||||
value={
|
||||
isNew ? "(Generato al salvataggio)" : formData.code
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
isNew
|
||||
? "Verrà assegnato automaticamente"
|
||||
: "Generato automaticamente"
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
sx={
|
||||
isNew
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 8 }}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice Alternativo"
|
||||
value={formData.alternativeCode}
|
||||
onChange={(e) =>
|
||||
handleChange("alternativeCode", e.target.value)
|
||||
}
|
||||
helperText="Opzionale"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Descrizione"
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
|
||||
const initialFormData = {
|
||||
code: "",
|
||||
alternativeCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: WarehouseType.Physical,
|
||||
@@ -77,6 +78,7 @@ export default function WarehouseLocationsPage() {
|
||||
setEditingWarehouse(warehouse);
|
||||
setFormData({
|
||||
code: warehouse.code,
|
||||
alternativeCode: warehouse.alternativeCode || "",
|
||||
name: warehouse.name,
|
||||
description: warehouse.description || "",
|
||||
type: warehouse.type,
|
||||
@@ -109,9 +111,7 @@ export default function WarehouseLocationsPage() {
|
||||
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = "Il codice è obbligatorio";
|
||||
}
|
||||
// Il codice è generato automaticamente, non richiede validazione in creazione
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = "Il nome è obbligatorio";
|
||||
}
|
||||
@@ -124,12 +124,16 @@ export default function WarehouseLocationsPage() {
|
||||
|
||||
try {
|
||||
if (editingWarehouse) {
|
||||
// In modifica non inviamo il code (non modificabile)
|
||||
const { code: _code, ...updateData } = formData;
|
||||
await updateMutation.mutateAsync({
|
||||
id: editingWarehouse.id,
|
||||
data: formData,
|
||||
data: updateData,
|
||||
});
|
||||
} else {
|
||||
await createMutation.mutateAsync(formData);
|
||||
// In creazione non inviamo il code (generato automaticamente dal backend)
|
||||
const { code: _code, ...createData } = formData;
|
||||
await createMutation.mutateAsync(createData);
|
||||
}
|
||||
handleCloseDialog();
|
||||
} catch (error) {
|
||||
@@ -364,19 +368,46 @@ export default function WarehouseLocationsPage() {
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice"
|
||||
value={formData.code}
|
||||
onChange={(e) => handleChange("code", e.target.value)}
|
||||
error={!!errors.code}
|
||||
helperText={errors.code}
|
||||
required
|
||||
disabled={!!editingWarehouse}
|
||||
value={
|
||||
editingWarehouse ? formData.code : "(Generato al salvataggio)"
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingWarehouse
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
sx={
|
||||
!editingWarehouse
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 8 }}>
|
||||
<Grid size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Codice Alternativo"
|
||||
value={formData.alternativeCode}
|
||||
onChange={(e) =>
|
||||
handleChange("alternativeCode", e.target.value)
|
||||
}
|
||||
helperText="Opzionale"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Nome"
|
||||
|
||||
@@ -117,6 +117,7 @@ export enum InventoryStatus {
|
||||
export interface WarehouseLocationDto {
|
||||
id: number;
|
||||
code: string;
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
address?: string;
|
||||
@@ -134,7 +135,8 @@ export interface WarehouseLocationDto {
|
||||
}
|
||||
|
||||
export interface CreateWarehouseDto {
|
||||
code: string;
|
||||
// code è generato automaticamente dal backend
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
address?: string;
|
||||
@@ -148,7 +150,20 @@ export interface CreateWarehouseDto {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateWarehouseDto extends CreateWarehouseDto {
|
||||
export interface UpdateWarehouseDto {
|
||||
// code non è modificabile
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
province?: string;
|
||||
postalCode?: string;
|
||||
country?: string;
|
||||
type: WarehouseType;
|
||||
isDefault: boolean;
|
||||
sortOrder: number;
|
||||
notes?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -159,6 +174,7 @@ export interface UpdateWarehouseDto extends CreateWarehouseDto {
|
||||
export interface CategoryDto {
|
||||
id: number;
|
||||
code: string;
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
parentCategoryId?: number;
|
||||
@@ -189,7 +205,8 @@ export interface CategoryTreeDto {
|
||||
}
|
||||
|
||||
export interface CreateCategoryDto {
|
||||
code: string;
|
||||
// code è generato automaticamente dal backend
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
parentCategoryId?: number;
|
||||
@@ -201,7 +218,8 @@ export interface CreateCategoryDto {
|
||||
}
|
||||
|
||||
export interface UpdateCategoryDto {
|
||||
code: string;
|
||||
// code non è modificabile
|
||||
alternativeCode?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
@@ -219,6 +237,7 @@ export interface UpdateCategoryDto {
|
||||
export interface ArticleDto {
|
||||
id: number;
|
||||
code: string;
|
||||
alternativeCode?: string;
|
||||
description: string;
|
||||
shortDescription?: string;
|
||||
barcode?: string;
|
||||
@@ -253,9 +272,10 @@ export interface ArticleDto {
|
||||
}
|
||||
|
||||
export interface CreateArticleDto {
|
||||
code: string;
|
||||
// code è generato automaticamente dal backend
|
||||
description: string;
|
||||
shortDescription?: string;
|
||||
alternativeCode?: string;
|
||||
barcode?: string;
|
||||
manufacturerCode?: string;
|
||||
categoryId?: number;
|
||||
@@ -280,7 +300,33 @@ export interface CreateArticleDto {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateArticleDto extends CreateArticleDto {
|
||||
export interface UpdateArticleDto {
|
||||
// code non è modificabile
|
||||
description: string;
|
||||
shortDescription?: string;
|
||||
alternativeCode?: string;
|
||||
barcode?: string;
|
||||
manufacturerCode?: string;
|
||||
categoryId?: number;
|
||||
unitOfMeasure: string;
|
||||
secondaryUnitOfMeasure?: string;
|
||||
unitConversionFactor?: number;
|
||||
stockManagement: StockManagementType;
|
||||
isBatchManaged: boolean;
|
||||
isSerialManaged: boolean;
|
||||
hasExpiry: boolean;
|
||||
expiryWarningDays?: number;
|
||||
minimumStock?: number;
|
||||
maximumStock?: number;
|
||||
reorderPoint?: number;
|
||||
reorderQuantity?: number;
|
||||
leadTimeDays?: number;
|
||||
valuationMethod?: ValuationMethod;
|
||||
standardCost?: number;
|
||||
baseSellingPrice?: number;
|
||||
weight?: number;
|
||||
volume?: number;
|
||||
notes?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -816,7 +862,7 @@ export function formatCurrency(value: number | undefined | null): string {
|
||||
|
||||
export function formatQuantity(
|
||||
value: number | undefined | null,
|
||||
decimals: number = 2
|
||||
decimals: number = 2,
|
||||
): string {
|
||||
if (value === undefined || value === null) return "-";
|
||||
return new Intl.NumberFormat("it-IT", {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -16,11 +16,15 @@ import {
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { articoliService, lookupService } from '../services/lookupService';
|
||||
import { Articolo } from '../types';
|
||||
} from "@mui/material";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { articoliService, lookupService } from "../services/lookupService";
|
||||
import { Articolo } from "../types";
|
||||
|
||||
export default function ArticoliPage() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -29,39 +33,40 @@ export default function ArticoliPage() {
|
||||
const [formData, setFormData] = useState<Partial<Articolo>>({ attivo: true });
|
||||
|
||||
const { data: articoli = [], isLoading } = useQuery({
|
||||
queryKey: ['articoli'],
|
||||
queryKey: ["articoli"],
|
||||
queryFn: () => articoliService.getAll(),
|
||||
});
|
||||
|
||||
const { data: tipiMateriale = [] } = useQuery({
|
||||
queryKey: ['lookup', 'tipi-materiale'],
|
||||
queryKey: ["lookup", "tipi-materiale"],
|
||||
queryFn: () => lookupService.getTipiMateriale(),
|
||||
});
|
||||
|
||||
const { data: categorie = [] } = useQuery({
|
||||
queryKey: ['lookup', 'categorie'],
|
||||
queryKey: ["lookup", "categorie"],
|
||||
queryFn: () => lookupService.getCategorie(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Articolo>) => articoliService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['articoli'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["articoli"] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Articolo> }) => articoliService.update(id, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Articolo> }) =>
|
||||
articoliService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['articoli'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["articoli"] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => articoliService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['articoli'] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["articoli"] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
@@ -78,35 +83,45 @@ export default function ArticoliPage() {
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
// In modifica, non inviamo il codice (non modificabile)
|
||||
const { codice: _codice, ...updateData } = formData;
|
||||
updateMutation.mutate({ id: editingId, data: updateData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
// In creazione, non inviamo il codice (generato automaticamente)
|
||||
const { codice: _codice, ...createData } = formData;
|
||||
createMutation.mutate(createData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'codice', headerName: 'Codice', width: 100 },
|
||||
{ field: 'descrizione', headerName: 'Descrizione', flex: 1, minWidth: 200 },
|
||||
{ field: "codice", headerName: "Codice", width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
|
||||
{ field: "descrizione", headerName: "Descrizione", flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: 'tipoMateriale',
|
||||
headerName: 'Tipo',
|
||||
field: "tipoMateriale",
|
||||
headerName: "Tipo",
|
||||
width: 130,
|
||||
valueGetter: (value: any) => value?.descrizione || '',
|
||||
valueGetter: (value: any) => value?.descrizione || "",
|
||||
},
|
||||
{
|
||||
field: 'categoria',
|
||||
headerName: 'Categoria',
|
||||
field: "categoria",
|
||||
headerName: "Categoria",
|
||||
width: 120,
|
||||
valueGetter: (value: any) => value?.descrizione || '',
|
||||
valueGetter: (value: any) => value?.descrizione || "",
|
||||
},
|
||||
{ field: 'qtaDisponibile', headerName: 'Disponibile', width: 100, type: 'number' },
|
||||
{ field: 'qtaStdA', headerName: 'Qta A', width: 80, type: 'number' },
|
||||
{ field: 'qtaStdB', headerName: 'Qta B', width: 80, type: 'number' },
|
||||
{ field: 'qtaStdS', headerName: 'Qta S', width: 80, type: 'number' },
|
||||
{ field: 'unitaMisura', headerName: 'UM', width: 60 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: 'Azioni',
|
||||
field: "qtaDisponibile",
|
||||
headerName: "Disponibile",
|
||||
width: 100,
|
||||
type: "number",
|
||||
},
|
||||
{ field: "qtaStdA", headerName: "Qta A", width: 80, type: "number" },
|
||||
{ field: "qtaStdB", headerName: "Qta B", width: 80, type: "number" },
|
||||
{ field: "qtaStdS", headerName: "Qta S", width: 80, type: "number" },
|
||||
{ field: "unitaMisura", headerName: "UM", width: 60 },
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Azioni",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -118,7 +133,7 @@ export default function ArticoliPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm('Eliminare questo articolo?')) {
|
||||
if (confirm("Eliminare questo articolo?")) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -132,14 +147,25 @@ export default function ArticoliPage() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Articoli</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpenDialog(true)}
|
||||
>
|
||||
Nuovo Articolo
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<Paper sx={{ height: 600, width: "100%" }}>
|
||||
<DataGrid
|
||||
rows={articoli}
|
||||
columns={columns}
|
||||
@@ -152,38 +178,89 @@ export default function ArticoliPage() {
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? 'Modifica Articolo' : 'Nuovo Articolo'}</DialogTitle>
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{editingId ? "Modifica Articolo" : "Nuovo Articolo"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice"
|
||||
fullWidth
|
||||
required
|
||||
value={formData.codice || ''}
|
||||
onChange={(e) => setFormData({ ...formData, codice: e.target.value })}
|
||||
value={
|
||||
editingId
|
||||
? formData.codice || ""
|
||||
: "(Generato al salvataggio)"
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingId
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
sx={
|
||||
!editingId
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice Alternativo"
|
||||
fullWidth
|
||||
value={formData.codiceAlternativo || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
codiceAlternativo: e.target.value,
|
||||
})
|
||||
}
|
||||
helperText="Opzionale"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
fullWidth
|
||||
required
|
||||
value={formData.descrizione || ''}
|
||||
onChange={(e) => setFormData({ ...formData, descrizione: e.target.value })}
|
||||
value={formData.descrizione || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, descrizione: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Tipo Materiale</InputLabel>
|
||||
<Select
|
||||
value={formData.tipoMaterialeId || ''}
|
||||
value={formData.tipoMaterialeId || ""}
|
||||
label="Tipo Materiale"
|
||||
onChange={(e) => setFormData({ ...formData, tipoMaterialeId: e.target.value as number })}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
tipoMaterialeId: e.target.value as number,
|
||||
})
|
||||
}
|
||||
>
|
||||
{tipiMateriale.map((t) => (
|
||||
<MenuItem key={t.id} value={t.id}>{t.descrizione}</MenuItem>
|
||||
<MenuItem key={t.id} value={t.id}>
|
||||
{t.descrizione}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -192,12 +269,19 @@ export default function ArticoliPage() {
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Categoria</InputLabel>
|
||||
<Select
|
||||
value={formData.categoriaId || ''}
|
||||
value={formData.categoriaId || ""}
|
||||
label="Categoria"
|
||||
onChange={(e) => setFormData({ ...formData, categoriaId: e.target.value as number })}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
categoriaId: e.target.value as number,
|
||||
})
|
||||
}
|
||||
>
|
||||
{categorie.map((c) => (
|
||||
<MenuItem key={c.id} value={c.id}>{c.descrizione}</MenuItem>
|
||||
<MenuItem key={c.id} value={c.id}>
|
||||
{c.descrizione}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -207,16 +291,23 @@ export default function ArticoliPage() {
|
||||
label="Quantità Disponibile"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaDisponibile || ''}
|
||||
onChange={(e) => setFormData({ ...formData, qtaDisponibile: parseFloat(e.target.value) || undefined })}
|
||||
value={formData.qtaDisponibile || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
qtaDisponibile: parseFloat(e.target.value) || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Unità Misura"
|
||||
fullWidth
|
||||
value={formData.unitaMisura || ''}
|
||||
onChange={(e) => setFormData({ ...formData, unitaMisura: e.target.value })}
|
||||
value={formData.unitaMisura || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, unitaMisura: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}></Grid>
|
||||
@@ -225,8 +316,13 @@ export default function ArticoliPage() {
|
||||
label="Qta Std Adulti (A)"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdA || ''}
|
||||
onChange={(e) => setFormData({ ...formData, qtaStdA: parseFloat(e.target.value) || undefined })}
|
||||
value={formData.qtaStdA || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
qtaStdA: parseFloat(e.target.value) || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
@@ -234,8 +330,13 @@ export default function ArticoliPage() {
|
||||
label="Qta Std Buffet (B)"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdB || ''}
|
||||
onChange={(e) => setFormData({ ...formData, qtaStdB: parseFloat(e.target.value) || undefined })}
|
||||
value={formData.qtaStdB || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
qtaStdB: parseFloat(e.target.value) || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
@@ -243,8 +344,13 @@ export default function ArticoliPage() {
|
||||
label="Qta Std Seduti (S)"
|
||||
fullWidth
|
||||
type="number"
|
||||
value={formData.qtaStdS || ''}
|
||||
onChange={(e) => setFormData({ ...formData, qtaStdS: parseFloat(e.target.value) || undefined })}
|
||||
value={formData.qtaStdS || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
qtaStdS: parseFloat(e.target.value) || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
@@ -253,8 +359,10 @@ export default function ArticoliPage() {
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
value={formData.note || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, note: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -262,7 +370,7 @@ export default function ArticoliPage() {
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? 'Salva' : 'Crea'}
|
||||
{editingId ? "Salva" : "Crea"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
793
frontend/src/pages/AutoCodesAdminPage.tsx
Normal file
793
frontend/src/pages/AutoCodesAdminPage.tsx
Normal file
@@ -0,0 +1,793 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
LinearProgress,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
TextField,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {
|
||||
Refresh as RefreshIcon,
|
||||
Edit as EditIcon,
|
||||
RestartAlt as ResetIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Code as CodeIcon,
|
||||
Preview as PreviewIcon,
|
||||
Help as HelpIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
} from "@mui/icons-material";
|
||||
import * as Icons from "@mui/icons-material";
|
||||
import { autoCodeService } from "../services/autoCodeService";
|
||||
import type {
|
||||
AutoCodeDto,
|
||||
AutoCodeUpdateDto,
|
||||
PlaceholderInfo,
|
||||
} from "../types/autoCode";
|
||||
import { groupByModule, moduleNames, moduleIcons } from "../types/autoCode";
|
||||
|
||||
export default function AutoCodesAdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [editingConfig, setEditingConfig] = useState<AutoCodeDto | null>(null);
|
||||
const [confirmReset, setConfirmReset] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [expandedModule, setExpandedModule] = useState<string | false>("core");
|
||||
|
||||
// Query per tutte le configurazioni
|
||||
const {
|
||||
data: configs = [],
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["autocodes"],
|
||||
queryFn: () => autoCodeService.getAll(),
|
||||
});
|
||||
|
||||
// Query per i placeholder disponibili
|
||||
const { data: placeholders = [] } = useQuery({
|
||||
queryKey: ["autocodes", "placeholders"],
|
||||
queryFn: () => autoCodeService.getPlaceholders(),
|
||||
});
|
||||
|
||||
// Mutation per aggiornare configurazione
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: AutoCodeUpdateDto }) =>
|
||||
autoCodeService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["autocodes"] });
|
||||
setEditingConfig(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation per reset sequenza
|
||||
const resetMutation = useMutation({
|
||||
mutationFn: (entityCode: string) =>
|
||||
autoCodeService.resetSequence(entityCode),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["autocodes"] });
|
||||
setConfirmReset(null);
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation per preview codice
|
||||
const previewMutation = useMutation({
|
||||
mutationFn: (entityCode: string) => autoCodeService.previewCode(entityCode),
|
||||
onSuccess: (data) => {
|
||||
setPreviewCode(data.code);
|
||||
},
|
||||
});
|
||||
|
||||
// Raggruppa configurazioni per modulo
|
||||
const groupedConfigs = groupByModule(configs);
|
||||
|
||||
// Helper per ottenere icona modulo
|
||||
const getModuleIcon = (moduleCode: string) => {
|
||||
const iconName = moduleIcons[moduleCode] || "Extension";
|
||||
const IconComponent = (Icons as Record<string, React.ComponentType>)[
|
||||
iconName
|
||||
];
|
||||
return IconComponent ? <IconComponent /> : <Icons.Extension />;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 3,
|
||||
flexWrap: "wrap",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Codici Automatici
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Configura i pattern per la generazione automatica dei codici
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<HelpIcon />}
|
||||
onClick={() => setShowHelp(true)}
|
||||
>
|
||||
Guida Pattern
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
Aggiorna
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Accordion per moduli */}
|
||||
{Object.entries(groupedConfigs).map(([moduleCode, moduleConfigs]) => (
|
||||
<Accordion
|
||||
key={moduleCode}
|
||||
expanded={expandedModule === moduleCode}
|
||||
onChange={(_, isExpanded) =>
|
||||
setExpandedModule(isExpanded ? moduleCode : false)
|
||||
}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{getModuleIcon(moduleCode)}
|
||||
<Typography variant="h6">
|
||||
{moduleNames[moduleCode] || moduleCode}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${moduleConfigs.length} configurazioni`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Entita</TableCell>
|
||||
<TableCell>Prefisso</TableCell>
|
||||
<TableCell>Pattern</TableCell>
|
||||
<TableCell>Esempio</TableCell>
|
||||
<TableCell>Sequenza</TableCell>
|
||||
<TableCell>Reset</TableCell>
|
||||
<TableCell align="center">Stato</TableCell>
|
||||
<TableCell align="right">Azioni</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{moduleConfigs.map((config) => (
|
||||
<TableRow key={config.id} hover>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{config.entityName}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{config.entityCode}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={config.prefix || "-"}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace", fontSize: "0.85rem" }}
|
||||
>
|
||||
{config.pattern}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
color: "primary.main",
|
||||
fontWeight: "medium",
|
||||
}}
|
||||
>
|
||||
{config.exampleCode}
|
||||
</Typography>
|
||||
<Tooltip title="Anteprima prossimo codice">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
previewMutation.mutate(config.entityCode)
|
||||
}
|
||||
disabled={!config.isEnabled}
|
||||
>
|
||||
<PreviewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{config.lastSequence}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{config.resetSequenceMonthly ? (
|
||||
<Chip label="Mensile" size="small" color="info" />
|
||||
) : config.resetSequenceYearly ? (
|
||||
<Chip label="Annuale" size="small" color="warning" />
|
||||
) : (
|
||||
<Chip label="Mai" size="small" variant="outlined" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={config.isEnabled ? "Attivo" : "Disattivo"}
|
||||
size="small"
|
||||
color={config.isEnabled ? "success" : "default"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Modifica">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setEditingConfig(config)}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset sequenza">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setConfirmReset(config.entityCode)}
|
||||
disabled={!config.isEnabled}
|
||||
>
|
||||
<ResetIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
|
||||
{/* Dialog modifica configurazione */}
|
||||
<EditConfigDialog
|
||||
config={editingConfig}
|
||||
placeholders={placeholders}
|
||||
onClose={() => setEditingConfig(null)}
|
||||
onSave={(data) => {
|
||||
if (editingConfig) {
|
||||
updateMutation.mutate({ id: editingConfig.id, data });
|
||||
}
|
||||
}}
|
||||
isSaving={updateMutation.isPending}
|
||||
error={updateMutation.error as Error | null}
|
||||
/>
|
||||
|
||||
{/* Dialog conferma reset */}
|
||||
<Dialog
|
||||
open={!!confirmReset}
|
||||
onClose={() => setConfirmReset(null)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Conferma Reset Sequenza</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Sei sicuro di voler resettare la sequenza per{" "}
|
||||
<strong>
|
||||
{configs.find((c) => c.entityCode === confirmReset)?.entityName}
|
||||
</strong>
|
||||
?
|
||||
</Typography>
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
La sequenza verra riportata a 0. Il prossimo codice generato partira
|
||||
da 1.
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setConfirmReset(null)}>Annulla</Button>
|
||||
<Button
|
||||
color="warning"
|
||||
variant="contained"
|
||||
onClick={() => confirmReset && resetMutation.mutate(confirmReset)}
|
||||
disabled={resetMutation.isPending}
|
||||
startIcon={
|
||||
resetMutation.isPending ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
<ResetIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog anteprima codice */}
|
||||
<Dialog
|
||||
open={!!previewCode}
|
||||
onClose={() => setPreviewCode(null)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Anteprima Prossimo Codice</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
p: 3,
|
||||
bgcolor: "grey.100",
|
||||
borderRadius: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<CodeIcon color="primary" />
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontFamily: "monospace", fontWeight: "bold" }}
|
||||
>
|
||||
{previewCode}
|
||||
</Typography>
|
||||
<Tooltip title="Copia">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(previewCode || "");
|
||||
}}
|
||||
>
|
||||
<CopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mt: 2, textAlign: "center" }}
|
||||
>
|
||||
Questo e il codice che verra generato alla prossima creazione.
|
||||
<br />
|
||||
La sequenza non e stata incrementata.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setPreviewCode(null)}>Chiudi</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Dialog guida pattern */}
|
||||
<Dialog
|
||||
open={showHelp}
|
||||
onClose={() => setShowHelp(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Guida ai Pattern</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography variant="body2" paragraph>
|
||||
I pattern definiscono come vengono generati i codici automatici.
|
||||
Puoi combinare testo statico e placeholder dinamici.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
|
||||
Placeholder Disponibili
|
||||
</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ mb: 3 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Placeholder</TableCell>
|
||||
<TableCell>Descrizione</TableCell>
|
||||
<TableCell>Esempio</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{placeholders.map((p) => (
|
||||
<TableRow key={p.placeholder}>
|
||||
<TableCell>
|
||||
<Typography sx={{ fontFamily: "monospace" }}>
|
||||
{p.placeholder}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{p.description}</TableCell>
|
||||
<TableCell>
|
||||
<Typography sx={{ fontFamily: "monospace" }}>
|
||||
{p.example}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Esempi di Pattern
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace", mb: 1 }}
|
||||
>
|
||||
{"{PREFIX}-{SEQ:5}"}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Risultato: ART-00001, ART-00002, ...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace", mb: 1 }}
|
||||
>
|
||||
{"{PREFIX}{YYYY}-{SEQ:5}"}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Risultato: EVT2025-00001 (reset annuale)
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace", mb: 1 }}
|
||||
>
|
||||
{"{PREFIX}{YY}{MM}-{SEQ:4}"}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Risultato: MOV2511-0001 (reset mensile)
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: "monospace", mb: 1 }}
|
||||
>
|
||||
{"FT{YYYY}/{SEQ:5}"}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Risultato: FT2025/00001 (formato fattura)
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowHelp(false)}>Chiudi</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog per modifica configurazione
|
||||
interface EditConfigDialogProps {
|
||||
config: AutoCodeDto | null;
|
||||
placeholders: PlaceholderInfo[];
|
||||
onClose: () => void;
|
||||
onSave: (data: AutoCodeUpdateDto) => void;
|
||||
isSaving: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
function EditConfigDialog({
|
||||
config,
|
||||
placeholders,
|
||||
onClose,
|
||||
onSave,
|
||||
isSaving,
|
||||
error,
|
||||
}: EditConfigDialogProps) {
|
||||
const [formData, setFormData] = useState<AutoCodeUpdateDto>({});
|
||||
|
||||
// Reset form quando cambia config
|
||||
const handleOpen = () => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
prefix: config.prefix,
|
||||
pattern: config.pattern,
|
||||
resetSequenceYearly: config.resetSequenceYearly,
|
||||
resetSequenceMonthly: config.resetSequenceMonthly,
|
||||
isEnabled: config.isEnabled,
|
||||
isReadOnly: config.isReadOnly,
|
||||
description: config.description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
// Calcola esempio in tempo reale
|
||||
const getExampleFromPattern = (pattern: string, prefix: string | null) => {
|
||||
const now = new Date();
|
||||
return pattern
|
||||
.replace("{PREFIX}", prefix || "")
|
||||
.replace("{YEAR}", now.getFullYear().toString())
|
||||
.replace("{YYYY}", now.getFullYear().toString())
|
||||
.replace("{YY}", now.getFullYear().toString().slice(-2))
|
||||
.replace("{MONTH}", (now.getMonth() + 1).toString().padStart(2, "0"))
|
||||
.replace("{MM}", (now.getMonth() + 1).toString().padStart(2, "0"))
|
||||
.replace("{DAY}", now.getDate().toString().padStart(2, "0"))
|
||||
.replace("{DD}", now.getDate().toString().padStart(2, "0"))
|
||||
.replace(/\{SEQ:(\d+)\}/g, (_, digits) => "X".repeat(parseInt(digits)));
|
||||
};
|
||||
|
||||
const currentExample = getExampleFromPattern(
|
||||
formData.pattern || config?.pattern || "",
|
||||
formData.prefix !== undefined ? formData.prefix : config?.prefix || null,
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!config}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
TransitionProps={{ onEntered: handleOpen }}
|
||||
>
|
||||
{config && (
|
||||
<>
|
||||
<DialogTitle>
|
||||
Modifica Configurazione: {config.entityName}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Prefisso"
|
||||
value={formData.prefix ?? config.prefix ?? ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, prefix: e.target.value || null })
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Testo sostituito nel placeholder {PREFIX}"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Pattern"
|
||||
value={formData.pattern ?? config.pattern}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, pattern: e.target.value })
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Pattern per generazione codice"
|
||||
InputProps={{
|
||||
sx: { fontFamily: "monospace" },
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip
|
||||
title={
|
||||
<Box>
|
||||
{placeholders.map((p) => (
|
||||
<Typography
|
||||
key={p.placeholder}
|
||||
variant="caption"
|
||||
display="block"
|
||||
>
|
||||
{p.placeholder}: {p.description}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<HelpIcon fontSize="small" color="action" />
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: "grey.100",
|
||||
borderRadius: 1,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Anteprima:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontFamily: "monospace", color: "primary.main" }}
|
||||
>
|
||||
{currentExample}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Reset Sequenza</InputLabel>
|
||||
<Select
|
||||
value={
|
||||
(formData.resetSequenceMonthly ??
|
||||
config.resetSequenceMonthly)
|
||||
? "monthly"
|
||||
: (formData.resetSequenceYearly ??
|
||||
config.resetSequenceYearly)
|
||||
? "yearly"
|
||||
: "never"
|
||||
}
|
||||
label="Reset Sequenza"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFormData({
|
||||
...formData,
|
||||
resetSequenceYearly: value === "yearly",
|
||||
resetSequenceMonthly: value === "monthly",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value="never">Mai</MenuItem>
|
||||
<MenuItem value="yearly">Ogni anno</MenuItem>
|
||||
<MenuItem value="monthly">Ogni mese</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
label="Descrizione"
|
||||
value={formData.description ?? config.description ?? ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
description: e.target.value || null,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
size="small"
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formData.isEnabled ?? config.isEnabled}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
isEnabled: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Generazione attiva"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 6 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formData.isReadOnly ?? config.isReadOnly}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
isReadOnly: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Codice non modificabile"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error.message || "Errore durante il salvataggio"}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Annulla</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
startIcon={
|
||||
isSaving ? <CircularProgress size={16} color="inherit" /> : null
|
||||
}
|
||||
>
|
||||
Salva
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -12,11 +12,15 @@ import {
|
||||
DialogActions,
|
||||
TextField,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||||
import { clientiService } from '../services/lookupService';
|
||||
import { Cliente } from '../types';
|
||||
} from "@mui/material";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { clientiService } from "../services/lookupService";
|
||||
import { Cliente } from "../types";
|
||||
|
||||
export default function ClientiPage() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -25,29 +29,30 @@ export default function ClientiPage() {
|
||||
const [formData, setFormData] = useState<Partial<Cliente>>({ attivo: true });
|
||||
|
||||
const { data: clienti = [], isLoading } = useQuery({
|
||||
queryKey: ['clienti'],
|
||||
queryKey: ["clienti"],
|
||||
queryFn: () => clientiService.getAll(),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Cliente>) => clientiService.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['clienti'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["clienti"] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Cliente> }) => clientiService.update(id, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Cliente> }) =>
|
||||
clientiService.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['clienti'] });
|
||||
queryClient.invalidateQueries({ queryKey: ["clienti"] });
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => clientiService.delete(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['clienti'] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["clienti"] }),
|
||||
});
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
@@ -64,22 +69,33 @@ export default function ClientiPage() {
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (editingId) {
|
||||
updateMutation.mutate({ id: editingId, data: formData });
|
||||
// In modifica, non inviamo il codice (non modificabile)
|
||||
const { codice: _codice, ...updateData } = formData;
|
||||
updateMutation.mutate({ id: editingId, data: updateData });
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
// In creazione, non inviamo il codice (generato automaticamente)
|
||||
const { codice: _codice, ...createData } = formData;
|
||||
createMutation.mutate(createData);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'ragioneSociale', headerName: 'Ragione Sociale', flex: 1, minWidth: 200 },
|
||||
{ field: 'citta', headerName: 'Città', width: 150 },
|
||||
{ field: 'provincia', headerName: 'Prov.', width: 80 },
|
||||
{ field: 'telefono', headerName: 'Telefono', width: 130 },
|
||||
{ field: 'email', headerName: 'Email', width: 200 },
|
||||
{ field: 'partitaIva', headerName: 'P.IVA', width: 130 },
|
||||
{ field: "codice", headerName: "Codice", width: 100 },
|
||||
{ field: "codiceAlternativo", headerName: "Cod. Alt.", width: 100 },
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: 'Azioni',
|
||||
field: "ragioneSociale",
|
||||
headerName: "Ragione Sociale",
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
},
|
||||
{ field: "citta", headerName: "Città", width: 150 },
|
||||
{ field: "provincia", headerName: "Prov.", width: 80 },
|
||||
{ field: "telefono", headerName: "Telefono", width: 130 },
|
||||
{ field: "email", headerName: "Email", width: 200 },
|
||||
{ field: "partitaIva", headerName: "P.IVA", width: 130 },
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Azioni",
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: (params) => (
|
||||
@@ -91,7 +107,7 @@ export default function ClientiPage() {
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (confirm('Eliminare questo cliente?')) {
|
||||
if (confirm("Eliminare questo cliente?")) {
|
||||
deleteMutation.mutate(params.row.id);
|
||||
}
|
||||
}}
|
||||
@@ -105,14 +121,25 @@ export default function ClientiPage() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Clienti</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpenDialog(true)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpenDialog(true)}
|
||||
>
|
||||
Nuovo Cliente
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ height: 600, width: '100%' }}>
|
||||
<Paper sx={{ height: 600, width: "100%" }}>
|
||||
<DataGrid
|
||||
rows={clienti}
|
||||
columns={columns}
|
||||
@@ -125,57 +152,120 @@ export default function ClientiPage() {
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{editingId ? 'Modifica Cliente' : 'Nuovo Cliente'}</DialogTitle>
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{editingId ? "Modifica Cliente" : "Nuovo Cliente"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice"
|
||||
fullWidth
|
||||
value={
|
||||
editingId
|
||||
? formData.codice || ""
|
||||
: "(Generato al salvataggio)"
|
||||
}
|
||||
disabled
|
||||
helperText={
|
||||
editingId
|
||||
? "Generato automaticamente"
|
||||
: "Verrà assegnato automaticamente"
|
||||
}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
sx={
|
||||
!editingId
|
||||
? {
|
||||
"& .MuiInputBase-input.Mui-disabled": {
|
||||
fontStyle: "italic",
|
||||
color: "text.secondary",
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<TextField
|
||||
label="Codice Alternativo"
|
||||
fullWidth
|
||||
value={formData.codiceAlternativo || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
codiceAlternativo: e.target.value,
|
||||
})
|
||||
}
|
||||
helperText="Opzionale"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Ragione Sociale"
|
||||
fullWidth
|
||||
required
|
||||
value={formData.ragioneSociale || ''}
|
||||
onChange={(e) => setFormData({ ...formData, ragioneSociale: e.target.value })}
|
||||
value={formData.ragioneSociale || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, ragioneSociale: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<TextField
|
||||
label="Indirizzo"
|
||||
fullWidth
|
||||
value={formData.indirizzo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, indirizzo: e.target.value })}
|
||||
value={formData.indirizzo || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, indirizzo: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="CAP"
|
||||
fullWidth
|
||||
value={formData.cap || ''}
|
||||
onChange={(e) => setFormData({ ...formData, cap: e.target.value })}
|
||||
value={formData.cap || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, cap: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<TextField
|
||||
label="Città"
|
||||
fullWidth
|
||||
value={formData.citta || ''}
|
||||
onChange={(e) => setFormData({ ...formData, citta: e.target.value })}
|
||||
value={formData.citta || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, citta: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<TextField
|
||||
label="Provincia"
|
||||
fullWidth
|
||||
value={formData.provincia || ''}
|
||||
onChange={(e) => setFormData({ ...formData, provincia: e.target.value })}
|
||||
value={formData.provincia || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, provincia: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Telefono"
|
||||
fullWidth
|
||||
value={formData.telefono || ''}
|
||||
onChange={(e) => setFormData({ ...formData, telefono: e.target.value })}
|
||||
value={formData.telefono || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, telefono: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
@@ -183,40 +273,53 @@ export default function ClientiPage() {
|
||||
label="Email"
|
||||
fullWidth
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
value={formData.email || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="PEC"
|
||||
fullWidth
|
||||
value={formData.pec || ''}
|
||||
onChange={(e) => setFormData({ ...formData, pec: e.target.value })}
|
||||
value={formData.pec || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, pec: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Codice Fiscale"
|
||||
fullWidth
|
||||
value={formData.codiceFiscale || ''}
|
||||
onChange={(e) => setFormData({ ...formData, codiceFiscale: e.target.value })}
|
||||
value={formData.codiceFiscale || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, codiceFiscale: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Partita IVA"
|
||||
fullWidth
|
||||
value={formData.partitaIva || ''}
|
||||
onChange={(e) => setFormData({ ...formData, partitaIva: e.target.value })}
|
||||
value={formData.partitaIva || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, partitaIva: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
label="Codice Destinatario"
|
||||
fullWidth
|
||||
value={formData.codiceDestinatario || ''}
|
||||
onChange={(e) => setFormData({ ...formData, codiceDestinatario: e.target.value })}
|
||||
value={formData.codiceDestinatario || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
codiceDestinatario: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
@@ -225,8 +328,10 @@ export default function ClientiPage() {
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={formData.note || ''}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
value={formData.note || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, note: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -234,7 +339,7 @@ export default function ClientiPage() {
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Annulla</Button>
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{editingId ? 'Salva' : 'Crea'}
|
||||
{editingId ? "Salva" : "Crea"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
97
frontend/src/services/autoCodeService.ts
Normal file
97
frontend/src/services/autoCodeService.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import api from "./api";
|
||||
import type {
|
||||
AutoCodeDto,
|
||||
AutoCodeUpdateDto,
|
||||
GenerateCodeResponse,
|
||||
ResetSequenceRequest,
|
||||
CheckUniqueResponse,
|
||||
PlaceholderInfo,
|
||||
} from "../types/autoCode";
|
||||
|
||||
/**
|
||||
* Service per la gestione dei codici automatici
|
||||
*/
|
||||
export const autoCodeService = {
|
||||
/**
|
||||
* Ottiene tutte le configurazioni AutoCode
|
||||
*/
|
||||
getAll: async (): Promise<AutoCodeDto[]> => {
|
||||
const response = await api.get("/autocodes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ottiene le configurazioni per un modulo specifico
|
||||
*/
|
||||
getByModule: async (moduleCode: string): Promise<AutoCodeDto[]> => {
|
||||
const response = await api.get(`/autocodes/module/${moduleCode}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ottiene una configurazione specifica per codice entita
|
||||
*/
|
||||
getByEntityCode: async (entityCode: string): Promise<AutoCodeDto> => {
|
||||
const response = await api.get(`/autocodes/${entityCode}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Genera un nuovo codice per un'entita
|
||||
*/
|
||||
generateCode: async (entityCode: string): Promise<GenerateCodeResponse> => {
|
||||
const response = await api.post(`/autocodes/${entityCode}/generate`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ottiene un'anteprima del prossimo codice senza incrementare la sequenza
|
||||
*/
|
||||
previewCode: async (entityCode: string): Promise<GenerateCodeResponse> => {
|
||||
const response = await api.get(`/autocodes/${entityCode}/preview`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Aggiorna una configurazione AutoCode
|
||||
*/
|
||||
update: async (id: number, data: AutoCodeUpdateDto): Promise<AutoCodeDto> => {
|
||||
const response = await api.put(`/autocodes/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resetta la sequenza per un'entita
|
||||
*/
|
||||
resetSequence: async (
|
||||
entityCode: string,
|
||||
request?: ResetSequenceRequest
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await api.post(`/autocodes/${entityCode}/reset-sequence`, request || {});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifica se un codice e unico per un'entita
|
||||
*/
|
||||
checkUnique: async (
|
||||
entityCode: string,
|
||||
code: string,
|
||||
excludeId?: number
|
||||
): Promise<CheckUniqueResponse> => {
|
||||
const response = await api.get(`/autocodes/${entityCode}/check-unique`, {
|
||||
params: { code, excludeId },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ottiene i placeholder disponibili per i pattern
|
||||
*/
|
||||
getPlaceholders: async (): Promise<PlaceholderInfo[]> => {
|
||||
const response = await api.get("/autocodes/placeholders");
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default autoCodeService;
|
||||
94
frontend/src/types/autoCode.ts
Normal file
94
frontend/src/types/autoCode.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Types per il sistema di codici automatici
|
||||
*/
|
||||
|
||||
export interface AutoCodeDto {
|
||||
id: number;
|
||||
entityCode: string;
|
||||
entityName: string;
|
||||
prefix: string | null;
|
||||
pattern: string;
|
||||
lastSequence: number;
|
||||
resetSequenceYearly: boolean;
|
||||
resetSequenceMonthly: boolean;
|
||||
lastResetYear: number | null;
|
||||
lastResetMonth: number | null;
|
||||
isEnabled: boolean;
|
||||
isReadOnly: boolean;
|
||||
moduleCode: string | null;
|
||||
description: string | null;
|
||||
sortOrder: number;
|
||||
exampleCode: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface AutoCodeUpdateDto {
|
||||
prefix?: string | null;
|
||||
pattern?: string | null;
|
||||
resetSequenceYearly?: boolean;
|
||||
resetSequenceMonthly?: boolean;
|
||||
isEnabled?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface GenerateCodeResponse {
|
||||
code: string;
|
||||
entityCode: string;
|
||||
isPreview?: boolean;
|
||||
}
|
||||
|
||||
export interface ResetSequenceRequest {
|
||||
newValue?: number;
|
||||
}
|
||||
|
||||
export interface CheckUniqueResponse {
|
||||
isUnique: boolean;
|
||||
code: string;
|
||||
entityCode: string;
|
||||
}
|
||||
|
||||
export interface PlaceholderInfo {
|
||||
placeholder: string;
|
||||
description: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raggruppa le configurazioni per modulo
|
||||
*/
|
||||
export function groupByModule(configs: AutoCodeDto[]): Record<string, AutoCodeDto[]> {
|
||||
return configs.reduce((acc, config) => {
|
||||
const module = config.moduleCode || "core";
|
||||
if (!acc[module]) {
|
||||
acc[module] = [];
|
||||
}
|
||||
acc[module].push(config);
|
||||
return acc;
|
||||
}, {} as Record<string, AutoCodeDto[]>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nomi visualizzati per i moduli
|
||||
*/
|
||||
export const moduleNames: Record<string, string> = {
|
||||
core: "Sistema Base",
|
||||
warehouse: "Magazzino",
|
||||
purchases: "Acquisti",
|
||||
sales: "Vendite",
|
||||
production: "Produzione",
|
||||
quality: "Qualità",
|
||||
};
|
||||
|
||||
/**
|
||||
* Icone per i moduli (nomi MUI icons)
|
||||
*/
|
||||
export const moduleIcons: Record<string, string> = {
|
||||
core: "Settings",
|
||||
warehouse: "Warehouse",
|
||||
purchases: "ShoppingCart",
|
||||
sales: "PointOfSale",
|
||||
production: "Factory",
|
||||
quality: "VerifiedUser",
|
||||
};
|
||||
@@ -13,6 +13,8 @@ export interface BaseEntity {
|
||||
}
|
||||
|
||||
export interface Cliente extends BaseEntity {
|
||||
codice: string;
|
||||
codiceAlternativo?: string;
|
||||
ragioneSociale: string;
|
||||
indirizzo?: string;
|
||||
cap?: string;
|
||||
@@ -89,6 +91,7 @@ export interface Risorsa extends BaseEntity {
|
||||
|
||||
export interface Articolo extends BaseEntity {
|
||||
codice: string;
|
||||
codiceAlternativo?: string;
|
||||
descrizione: string;
|
||||
tipoMaterialeId?: number;
|
||||
tipoMateriale?: TipoMateriale;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
|
||||
public class ArticoliController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public ArticoliController(AppollinareDbContext context)
|
||||
public ArticoliController(AppollinareDbContext context, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -60,6 +63,13 @@ public class ArticoliController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Articolo>> CreateArticolo(Articolo articolo)
|
||||
{
|
||||
// Genera codice automatico
|
||||
var codice = await _autoCodeService.GenerateNextCodeAsync("articolo");
|
||||
if (!string.IsNullOrEmpty(codice))
|
||||
{
|
||||
articolo.Codice = codice;
|
||||
}
|
||||
|
||||
articolo.CreatedAt = DateTime.UtcNow;
|
||||
_context.Articoli.Add(articolo);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
240
src/Apollinare.API/Controllers/AutoCodesController.cs
Normal file
240
src/Apollinare.API/Controllers/AutoCodesController.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Apollinare.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AutoCodesController : ControllerBase
|
||||
{
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
private readonly ILogger<AutoCodesController> _logger;
|
||||
|
||||
public AutoCodesController(AutoCodeService autoCodeService, ILogger<AutoCodesController> logger)
|
||||
{
|
||||
_autoCodeService = autoCodeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutte le configurazioni AutoCode.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<AutoCodeDto>>> GetAll()
|
||||
{
|
||||
var configs = await _autoCodeService.GetAllConfigurationsAsync();
|
||||
return Ok(configs.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le configurazioni AutoCode per un modulo specifico.
|
||||
/// </summary>
|
||||
[HttpGet("module/{moduleCode}")]
|
||||
public async Task<ActionResult<List<AutoCodeDto>>> GetByModule(string moduleCode)
|
||||
{
|
||||
var configs = await _autoCodeService.GetConfigurationsByModuleAsync(moduleCode);
|
||||
return Ok(configs.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene una configurazione AutoCode specifica.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}")]
|
||||
public async Task<ActionResult<AutoCodeDto>> Get(string entityCode)
|
||||
{
|
||||
var config = await _autoCodeService.GetConfigurationAsync(entityCode);
|
||||
if (config == null)
|
||||
return NotFound($"Configurazione per '{entityCode}' non trovata");
|
||||
|
||||
return Ok(ToDto(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera un nuovo codice per un'entità specifica.
|
||||
/// </summary>
|
||||
[HttpPost("{entityCode}/generate")]
|
||||
public async Task<ActionResult<GenerateCodeResponse>> GenerateCode(string entityCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var code = await _autoCodeService.GenerateNextCodeAsync(entityCode);
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
|
||||
}
|
||||
|
||||
return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
|
||||
return StatusCode(500, new { error = "Errore durante la generazione del codice" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un'anteprima del prossimo codice senza incrementare la sequenza.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}/preview")]
|
||||
public async Task<ActionResult<GenerateCodeResponse>> PreviewCode(string entityCode)
|
||||
{
|
||||
var code = await _autoCodeService.PreviewNextCodeAsync(entityCode);
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
|
||||
}
|
||||
|
||||
return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode, IsPreview = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna una configurazione AutoCode.
|
||||
/// </summary>
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<ActionResult<AutoCodeDto>> Update(int id, [FromBody] AutoCodeUpdateDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _autoCodeService.UpdateConfigurationAsync(id, dto);
|
||||
return Ok(ToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta la sequenza per un'entità specifica.
|
||||
/// </summary>
|
||||
[HttpPost("{entityCode}/reset-sequence")]
|
||||
public async Task<ActionResult> ResetSequence(string entityCode, [FromBody] ResetSequenceRequest? request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _autoCodeService.ResetSequenceAsync(entityCode, request?.NewValue ?? 0);
|
||||
return Ok(new { message = $"Sequenza resettata per '{entityCode}'" });
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se un codice è unico per un'entità.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}/check-unique")]
|
||||
public async Task<ActionResult<CheckUniqueResponse>> CheckUnique(
|
||||
string entityCode,
|
||||
[FromQuery] string code,
|
||||
[FromQuery] int? excludeId = null)
|
||||
{
|
||||
var isUnique = await _autoCodeService.IsCodeUniqueAsync(entityCode, code, excludeId);
|
||||
return Ok(new CheckUniqueResponse { IsUnique = isUnique, Code = code, EntityCode = entityCode });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene i placeholder disponibili per i pattern.
|
||||
/// </summary>
|
||||
[HttpGet("placeholders")]
|
||||
public ActionResult<List<PlaceholderInfo>> GetPlaceholders()
|
||||
{
|
||||
var placeholders = new List<PlaceholderInfo>
|
||||
{
|
||||
new() { Placeholder = "{PREFIX}", Description = "Prefisso configurato", Example = "ART" },
|
||||
new() { Placeholder = "{SEQ:n}", Description = "Sequenza numerica con n cifre", Example = "{SEQ:5} → 00001" },
|
||||
new() { Placeholder = "{YYYY}", Description = "Anno a 4 cifre", Example = "2025" },
|
||||
new() { Placeholder = "{YY}", Description = "Anno a 2 cifre", Example = "25" },
|
||||
new() { Placeholder = "{MM}", Description = "Mese a 2 cifre", Example = "11" },
|
||||
new() { Placeholder = "{DD}", Description = "Giorno a 2 cifre", Example = "29" },
|
||||
new() { Placeholder = "{YEAR}", Description = "Alias per {YYYY}", Example = "2025" },
|
||||
new() { Placeholder = "{MONTH}", Description = "Alias per {MM}", Example = "11" },
|
||||
new() { Placeholder = "{DAY}", Description = "Alias per {DD}", Example = "29" },
|
||||
};
|
||||
|
||||
return Ok(placeholders);
|
||||
}
|
||||
|
||||
private static AutoCodeDto ToDto(AutoCode entity)
|
||||
{
|
||||
return new AutoCodeDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
EntityCode = entity.EntityCode,
|
||||
EntityName = entity.EntityName,
|
||||
Prefix = entity.Prefix,
|
||||
Pattern = entity.Pattern,
|
||||
LastSequence = entity.LastSequence,
|
||||
ResetSequenceYearly = entity.ResetSequenceYearly,
|
||||
ResetSequenceMonthly = entity.ResetSequenceMonthly,
|
||||
LastResetYear = entity.LastResetYear,
|
||||
LastResetMonth = entity.LastResetMonth,
|
||||
IsEnabled = entity.IsEnabled,
|
||||
IsReadOnly = entity.IsReadOnly,
|
||||
ModuleCode = entity.ModuleCode,
|
||||
Description = entity.Description,
|
||||
SortOrder = entity.SortOrder,
|
||||
ExampleCode = entity.GetExampleCode(),
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region DTOs
|
||||
|
||||
public class AutoCodeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
public string EntityName { get; set; } = string.Empty;
|
||||
public string? Prefix { get; set; }
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
public long LastSequence { get; set; }
|
||||
public bool ResetSequenceYearly { get; set; }
|
||||
public bool ResetSequenceMonthly { get; set; }
|
||||
public int? LastResetYear { get; set; }
|
||||
public int? LastResetMonth { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsReadOnly { get; set; }
|
||||
public string? ModuleCode { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public string ExampleCode { get; set; } = string.Empty;
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class GenerateCodeResponse
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
public bool IsPreview { get; set; }
|
||||
}
|
||||
|
||||
public class ResetSequenceRequest
|
||||
{
|
||||
public long NewValue { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class CheckUniqueResponse
|
||||
{
|
||||
public bool IsUnique { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PlaceholderInfo
|
||||
{
|
||||
public string Placeholder { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Example { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
|
||||
public class ClientiController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public ClientiController(AppollinareDbContext context)
|
||||
public ClientiController(AppollinareDbContext context, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -47,6 +50,13 @@ public class ClientiController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Cliente>> CreateCliente(Cliente cliente)
|
||||
{
|
||||
// Genera codice automatico
|
||||
var codice = await _autoCodeService.GenerateNextCodeAsync("cliente");
|
||||
if (!string.IsNullOrEmpty(codice))
|
||||
{
|
||||
cliente.Codice = codice;
|
||||
}
|
||||
|
||||
cliente.CreatedAt = DateTime.UtcNow;
|
||||
_context.Clienti.Add(cliente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Apollinare.API.Hubs;
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Domain.Enums;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
@@ -13,11 +14,13 @@ public class EventiController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly DataNotificationService _notifier;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public EventiController(AppollinareDbContext context, DataNotificationService notifier)
|
||||
public EventiController(AppollinareDbContext context, DataNotificationService notifier, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_notifier = notifier;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -343,6 +346,12 @@ public class EventiController : ControllerBase
|
||||
|
||||
private async Task<string> GeneraCodiceEvento()
|
||||
{
|
||||
// Usa AutoCodeService per generare il codice
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("evento");
|
||||
if (generatedCode != null)
|
||||
return generatedCode;
|
||||
|
||||
// Fallback: metodo legacy
|
||||
var anno = DateTime.Now.Year;
|
||||
var ultimoEvento = await _context.Eventi
|
||||
.Where(e => e.Codice != null && e.Codice.StartsWith($"EV{anno}"))
|
||||
|
||||
@@ -239,6 +239,7 @@ public class WarehouseArticlesController : ControllerBase
|
||||
public record ArticleDto(
|
||||
int Id,
|
||||
string Code,
|
||||
string? AlternativeCode,
|
||||
string Description,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
@@ -273,36 +274,36 @@ public class WarehouseArticlesController : ControllerBase
|
||||
);
|
||||
|
||||
public record CreateArticleDto(
|
||||
string Code,
|
||||
string Description,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
string? ManufacturerCode,
|
||||
int? CategoryId,
|
||||
string UnitOfMeasure,
|
||||
string? SecondaryUnitOfMeasure,
|
||||
decimal? UnitConversionFactor,
|
||||
StockManagementType StockManagement,
|
||||
bool IsBatchManaged,
|
||||
bool IsSerialManaged,
|
||||
bool HasExpiry,
|
||||
int? ExpiryWarningDays,
|
||||
decimal? MinimumStock,
|
||||
decimal? MaximumStock,
|
||||
decimal? ReorderPoint,
|
||||
decimal? ReorderQuantity,
|
||||
int? LeadTimeDays,
|
||||
ValuationMethod? ValuationMethod,
|
||||
decimal? StandardCost,
|
||||
decimal? BaseSellingPrice,
|
||||
decimal? Weight,
|
||||
decimal? Volume,
|
||||
string? Notes
|
||||
string? AlternativeCode = null,
|
||||
string? ShortDescription = null,
|
||||
string? Barcode = null,
|
||||
string? ManufacturerCode = null,
|
||||
int? CategoryId = null,
|
||||
string? SecondaryUnitOfMeasure = null,
|
||||
decimal? UnitConversionFactor = null,
|
||||
StockManagementType StockManagement = StockManagementType.Standard,
|
||||
bool IsBatchManaged = false,
|
||||
bool IsSerialManaged = false,
|
||||
bool HasExpiry = false,
|
||||
int? ExpiryWarningDays = null,
|
||||
decimal? MinimumStock = null,
|
||||
decimal? MaximumStock = null,
|
||||
decimal? ReorderPoint = null,
|
||||
decimal? ReorderQuantity = null,
|
||||
int? LeadTimeDays = null,
|
||||
ValuationMethod? ValuationMethod = null,
|
||||
decimal? StandardCost = null,
|
||||
decimal? BaseSellingPrice = null,
|
||||
decimal? Weight = null,
|
||||
decimal? Volume = null,
|
||||
string? Notes = null
|
||||
);
|
||||
|
||||
public record UpdateArticleDto(
|
||||
string Code,
|
||||
string Description,
|
||||
string? AlternativeCode,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
string? ManufacturerCode,
|
||||
@@ -363,6 +364,7 @@ public class WarehouseArticlesController : ControllerBase
|
||||
private static ArticleDto MapToDto(WarehouseArticle article) => new(
|
||||
article.Id,
|
||||
article.Code,
|
||||
article.AlternativeCode,
|
||||
article.Description,
|
||||
article.ShortDescription,
|
||||
article.Barcode,
|
||||
@@ -398,7 +400,8 @@ public class WarehouseArticlesController : ControllerBase
|
||||
|
||||
private static WarehouseArticle MapFromDto(CreateArticleDto dto) => new()
|
||||
{
|
||||
Code = dto.Code,
|
||||
// Code viene generato automaticamente da WarehouseService.CreateArticleAsync
|
||||
AlternativeCode = dto.AlternativeCode,
|
||||
Description = dto.Description,
|
||||
ShortDescription = dto.ShortDescription,
|
||||
Barcode = dto.Barcode,
|
||||
@@ -428,7 +431,8 @@ public class WarehouseArticlesController : ControllerBase
|
||||
|
||||
private static void UpdateFromDto(WarehouseArticle article, UpdateArticleDto dto)
|
||||
{
|
||||
article.Code = dto.Code;
|
||||
// Code non viene aggiornato - è generato automaticamente e immutabile
|
||||
article.AlternativeCode = dto.AlternativeCode;
|
||||
article.Description = dto.Description;
|
||||
article.ShortDescription = dto.ShortDescription;
|
||||
article.Barcode = dto.Barcode;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities.Warehouse;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -13,6 +14,7 @@ public class WarehouseService : IWarehouseService
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<WarehouseService> _logger;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
private const string WAREHOUSES_CACHE_KEY = "warehouse_locations";
|
||||
private const string CATEGORIES_CACHE_KEY = "warehouse_categories";
|
||||
@@ -22,11 +24,13 @@ public class WarehouseService : IWarehouseService
|
||||
public WarehouseService(
|
||||
AppollinareDbContext context,
|
||||
IMemoryCache cache,
|
||||
ILogger<WarehouseService> logger)
|
||||
ILogger<WarehouseService> logger,
|
||||
AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
#region Articoli
|
||||
@@ -118,6 +122,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseArticle> CreateArticleAsync(WarehouseArticle article)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(article.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_article");
|
||||
if (generatedCode != null)
|
||||
article.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per l'articolo");
|
||||
}
|
||||
|
||||
// Verifica unicità codice
|
||||
if (await _context.WarehouseArticles.AnyAsync(a => a.Code == article.Code))
|
||||
throw new InvalidOperationException($"Esiste già un articolo con codice '{article.Code}'");
|
||||
@@ -230,6 +244,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseArticleCategory> CreateCategoryAsync(WarehouseArticleCategory category)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(category.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_category");
|
||||
if (generatedCode != null)
|
||||
category.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per la categoria");
|
||||
}
|
||||
|
||||
if (await _context.WarehouseArticleCategories.AnyAsync(c => c.Code == category.Code))
|
||||
throw new InvalidOperationException($"Esiste già una categoria con codice '{category.Code}'");
|
||||
|
||||
@@ -336,6 +360,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseLocation> CreateWarehouseAsync(WarehouseLocation warehouse)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(warehouse.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_location");
|
||||
if (generatedCode != null)
|
||||
warehouse.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per il magazzino");
|
||||
}
|
||||
|
||||
if (await _context.WarehouseLocations.AnyAsync(w => w.Code == warehouse.Code))
|
||||
throw new InvalidOperationException($"Esiste già un magazzino con codice '{warehouse.Code}'");
|
||||
|
||||
@@ -464,6 +498,16 @@ public class WarehouseService : IWarehouseService
|
||||
if (!article.IsBatchManaged)
|
||||
throw new InvalidOperationException("L'articolo non è gestito a lotti");
|
||||
|
||||
// Genera numero lotto automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(batch.BatchNumber))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("article_batch");
|
||||
if (generatedCode != null)
|
||||
batch.BatchNumber = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare numero lotto automatico");
|
||||
}
|
||||
|
||||
// Verifica unicità batch number per articolo
|
||||
if (await _context.ArticleBatches.AnyAsync(b => b.ArticleId == batch.ArticleId && b.BatchNumber == batch.BatchNumber))
|
||||
throw new InvalidOperationException($"Esiste già un lotto '{batch.BatchNumber}' per questo articolo");
|
||||
@@ -809,9 +853,15 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<StockMovement> CreateMovementAsync(StockMovement movement)
|
||||
{
|
||||
// Genera numero documento se non specificato
|
||||
// Genera numero documento automaticamente se non specificato
|
||||
if (string.IsNullOrEmpty(movement.DocumentNumber))
|
||||
movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type);
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("stock_movement");
|
||||
if (generatedCode != null)
|
||||
movement.DocumentNumber = generatedCode;
|
||||
else
|
||||
movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type); // Fallback
|
||||
}
|
||||
|
||||
// Verifica unicità documento
|
||||
if (await _context.StockMovements.AnyAsync(m => m.DocumentNumber == movement.DocumentNumber))
|
||||
@@ -1428,8 +1478,15 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<InventoryCount> CreateInventoryCountAsync(InventoryCount inventory)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrEmpty(inventory.Code))
|
||||
inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}";
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("inventory_count");
|
||||
if (generatedCode != null)
|
||||
inventory.Code = generatedCode;
|
||||
else
|
||||
inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}"; // Fallback
|
||||
}
|
||||
|
||||
inventory.CreatedAt = DateTime.UtcNow;
|
||||
_context.InventoryCounts.Add(inventory);
|
||||
|
||||
@@ -19,6 +19,7 @@ builder.Services.AddScoped<EventoCostiService>();
|
||||
builder.Services.AddScoped<DemoDataService>();
|
||||
builder.Services.AddScoped<ReportGeneratorService>();
|
||||
builder.Services.AddScoped<ModuleService>();
|
||||
builder.Services.AddScoped<AutoCodeService>();
|
||||
builder.Services.AddSingleton<DataNotificationService>();
|
||||
|
||||
// Warehouse Module Services
|
||||
@@ -100,6 +101,10 @@ using (var scope = app.Services.CreateScope())
|
||||
// Seed warehouse default data
|
||||
var warehouseService = scope.ServiceProvider.GetRequiredService<IWarehouseService>();
|
||||
await warehouseService.SeedDefaultDataAsync();
|
||||
|
||||
// Seed AutoCode configurations
|
||||
var autoCodeService = scope.ServiceProvider.GetRequiredService<AutoCodeService>();
|
||||
await autoCodeService.SeedDefaultConfigurationsAsync();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
||||
489
src/Apollinare.API/Services/AutoCodeService.cs
Normal file
489
src/Apollinare.API/Services/AutoCodeService.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Apollinare.API.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Servizio per la generazione automatica di codici univoci.
|
||||
/// Thread-safe grazie all'uso di transazioni database.
|
||||
/// </summary>
|
||||
public class AutoCodeService
|
||||
{
|
||||
private readonly AppollinareDbContext _db;
|
||||
private readonly ILogger<AutoCodeService> _logger;
|
||||
private static readonly Regex SequencePattern = new(@"\{SEQ:(\d+)\}", RegexOptions.Compiled);
|
||||
|
||||
public AutoCodeService(AppollinareDbContext db, ILogger<AutoCodeService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera il prossimo codice per un'entità specifica.
|
||||
/// Incrementa automaticamente la sequenza e gestisce i reset periodici.
|
||||
/// </summary>
|
||||
/// <param name="entityCode">Codice dell'entità (es. "warehouse_article")</param>
|
||||
/// <returns>Il nuovo codice generato, o null se la generazione automatica è disabilitata</returns>
|
||||
public async Task<string?> GenerateNextCodeAsync(string entityCode)
|
||||
{
|
||||
// Usa una transazione per garantire atomicità
|
||||
await using var transaction = await _db.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
_logger.LogWarning("Configurazione AutoCode non trovata per entità: {EntityCode}", entityCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!config.IsEnabled)
|
||||
{
|
||||
_logger.LogDebug("Generazione automatica disabilitata per entità: {EntityCode}", entityCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Gestione reset sequenza
|
||||
if (ShouldResetSequence(config, now))
|
||||
{
|
||||
config.LastSequence = 0;
|
||||
config.LastResetYear = now.Year;
|
||||
config.LastResetMonth = now.Month;
|
||||
_logger.LogInformation("Sequenza resettata per entità {EntityCode} (Anno: {Year}, Mese: {Month})",
|
||||
entityCode, now.Year, now.Month);
|
||||
}
|
||||
|
||||
// Incrementa sequenza
|
||||
config.LastSequence++;
|
||||
config.UpdatedAt = now;
|
||||
|
||||
// Genera codice dal pattern
|
||||
var code = GenerateCodeFromPattern(config.Pattern, config.Prefix, config.LastSequence, now);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogDebug("Codice generato per {EntityCode}: {Code}", entityCode, code);
|
||||
return code;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera un codice di anteprima senza incrementare la sequenza.
|
||||
/// Utile per mostrare all'utente cosa verrà generato.
|
||||
/// </summary>
|
||||
public async Task<string?> PreviewNextCodeAsync(string entityCode)
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
|
||||
if (config == null || !config.IsEnabled)
|
||||
return null;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var nextSequence = config.LastSequence + 1;
|
||||
|
||||
// Simula reset se necessario
|
||||
if (ShouldResetSequence(config, now))
|
||||
nextSequence = 1;
|
||||
|
||||
return GenerateCodeFromPattern(config.Pattern, config.Prefix, nextSequence, now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se un codice è già utilizzato per un'entità.
|
||||
/// </summary>
|
||||
public async Task<bool> IsCodeUniqueAsync(string entityCode, string code, int? excludeId = null)
|
||||
{
|
||||
return entityCode switch
|
||||
{
|
||||
"warehouse_article" => !await _db.WarehouseArticles
|
||||
.AnyAsync(a => a.Code == code && (excludeId == null || a.Id != excludeId)),
|
||||
|
||||
"warehouse_location" => !await _db.WarehouseLocations
|
||||
.AnyAsync(w => w.Code == code && (excludeId == null || w.Id != excludeId)),
|
||||
|
||||
"warehouse_category" => !await _db.WarehouseArticleCategories
|
||||
.AnyAsync(c => c.Code == code && (excludeId == null || c.Id != excludeId)),
|
||||
|
||||
"stock_movement" => !await _db.StockMovements
|
||||
.AnyAsync(m => m.DocumentNumber == code && (excludeId == null || m.Id != excludeId)),
|
||||
|
||||
"inventory_count" => !await _db.InventoryCounts
|
||||
.AnyAsync(i => i.Code == code && (excludeId == null || i.Id != excludeId)),
|
||||
|
||||
"cliente" => !await _db.Clienti
|
||||
.AnyAsync(c => c.Codice == code && (excludeId == null || c.Id != excludeId)),
|
||||
|
||||
"evento" => !await _db.Eventi
|
||||
.AnyAsync(e => e.Codice == code && (excludeId == null || e.Id != excludeId)),
|
||||
|
||||
"articolo" => !await _db.Articoli
|
||||
.AnyAsync(a => a.Codice == code && (excludeId == null || a.Id != excludeId)),
|
||||
|
||||
_ => true // Entità non gestita, assume codice unico
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutte le configurazioni AutoCode.
|
||||
/// </summary>
|
||||
public async Task<List<AutoCode>> GetAllConfigurationsAsync()
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.OrderBy(c => c.ModuleCode)
|
||||
.ThenBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.EntityName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le configurazioni AutoCode per un modulo specifico.
|
||||
/// </summary>
|
||||
public async Task<List<AutoCode>> GetConfigurationsByModuleAsync(string moduleCode)
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.Where(c => c.ModuleCode == moduleCode)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.EntityName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene una configurazione AutoCode per codice entità.
|
||||
/// </summary>
|
||||
public async Task<AutoCode?> GetConfigurationAsync(string entityCode)
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna una configurazione AutoCode.
|
||||
/// </summary>
|
||||
public async Task<AutoCode> UpdateConfigurationAsync(int id, AutoCodeUpdateDto dto)
|
||||
{
|
||||
var config = await _db.AutoCodes.FindAsync(id)
|
||||
?? throw new KeyNotFoundException($"Configurazione AutoCode con ID {id} non trovata");
|
||||
|
||||
if (dto.Prefix != null)
|
||||
config.Prefix = dto.Prefix;
|
||||
|
||||
if (dto.Pattern != null)
|
||||
{
|
||||
// Valida il pattern
|
||||
ValidatePattern(dto.Pattern);
|
||||
config.Pattern = dto.Pattern;
|
||||
}
|
||||
|
||||
if (dto.ResetSequenceYearly.HasValue)
|
||||
config.ResetSequenceYearly = dto.ResetSequenceYearly.Value;
|
||||
|
||||
if (dto.ResetSequenceMonthly.HasValue)
|
||||
config.ResetSequenceMonthly = dto.ResetSequenceMonthly.Value;
|
||||
|
||||
if (dto.IsEnabled.HasValue)
|
||||
config.IsEnabled = dto.IsEnabled.Value;
|
||||
|
||||
if (dto.IsReadOnly.HasValue)
|
||||
config.IsReadOnly = dto.IsReadOnly.Value;
|
||||
|
||||
if (dto.Description != null)
|
||||
config.Description = dto.Description;
|
||||
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta manualmente la sequenza per un'entità.
|
||||
/// </summary>
|
||||
public async Task ResetSequenceAsync(string entityCode, long newValue = 0)
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode)
|
||||
?? throw new KeyNotFoundException($"Configurazione AutoCode per '{entityCode}' non trovata");
|
||||
|
||||
config.LastSequence = newValue;
|
||||
config.LastResetYear = DateTime.Now.Year;
|
||||
config.LastResetMonth = DateTime.Now.Month;
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Sequenza resettata manualmente per {EntityCode} a {Value}", entityCode, newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inizializza le configurazioni di default per tutte le entità.
|
||||
/// Chiamato al seed dell'applicazione.
|
||||
/// </summary>
|
||||
public async Task SeedDefaultConfigurationsAsync()
|
||||
{
|
||||
var defaults = new List<AutoCode>
|
||||
{
|
||||
// Core
|
||||
new()
|
||||
{
|
||||
EntityCode = "cliente",
|
||||
EntityName = "Cliente",
|
||||
Prefix = "CLI",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "core",
|
||||
Description = "Codice cliente (es. CLI-00001)",
|
||||
SortOrder = 10
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "evento",
|
||||
EntityName = "Evento",
|
||||
Prefix = "EVT",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "core",
|
||||
Description = "Codice evento con anno (es. EVT2025-00001)",
|
||||
SortOrder = 20
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "articolo",
|
||||
EntityName = "Articolo (Legacy)",
|
||||
Prefix = "ART",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "core",
|
||||
Description = "Codice articolo legacy (es. ART-00001)",
|
||||
SortOrder = 30
|
||||
},
|
||||
|
||||
// Warehouse module
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_location",
|
||||
EntityName = "Magazzino",
|
||||
Prefix = "MAG",
|
||||
Pattern = "{PREFIX}-{SEQ:3}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice magazzino (es. MAG-001)",
|
||||
SortOrder = 10
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_article",
|
||||
EntityName = "Articolo Magazzino",
|
||||
Prefix = "WA",
|
||||
Pattern = "{PREFIX}{SEQ:6}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice articolo magazzino (es. WA000001)",
|
||||
SortOrder = 20
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_category",
|
||||
EntityName = "Categoria Articolo",
|
||||
Prefix = "CAT",
|
||||
Pattern = "{PREFIX}-{SEQ:4}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice categoria (es. CAT-0001)",
|
||||
SortOrder = 30
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "stock_movement",
|
||||
EntityName = "Movimento Magazzino",
|
||||
Prefix = "MOV",
|
||||
Pattern = "{PREFIX}{YYYY}{MM}-{SEQ:5}",
|
||||
ResetSequenceMonthly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Numero documento movimento (es. MOV202511-00001)",
|
||||
SortOrder = 40
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "inventory_count",
|
||||
EntityName = "Inventario",
|
||||
Prefix = "INV",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:4}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice inventario (es. INV2025-0001)",
|
||||
SortOrder = 50
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "article_batch",
|
||||
EntityName = "Lotto Articolo",
|
||||
Prefix = "LOT",
|
||||
Pattern = "{PREFIX}{YYYY}{MM}{DD}-{SEQ:4}",
|
||||
ResetSequenceMonthly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Numero lotto (es. LOT20251129-0001)",
|
||||
SortOrder = 60
|
||||
},
|
||||
|
||||
// Future: Purchases module
|
||||
new()
|
||||
{
|
||||
EntityCode = "purchase_order",
|
||||
EntityName = "Ordine Acquisto",
|
||||
Prefix = "ODA",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "purchases",
|
||||
Description = "Numero ordine acquisto (es. ODA2025-00001)",
|
||||
SortOrder = 10,
|
||||
IsEnabled = false // Disabilitato finché il modulo non è implementato
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "supplier",
|
||||
EntityName = "Fornitore",
|
||||
Prefix = "FOR",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "purchases",
|
||||
Description = "Codice fornitore (es. FOR-00001)",
|
||||
SortOrder = 20,
|
||||
IsEnabled = false
|
||||
},
|
||||
|
||||
// Future: Sales module
|
||||
new()
|
||||
{
|
||||
EntityCode = "sales_order",
|
||||
EntityName = "Ordine Vendita",
|
||||
Prefix = "ODV",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "sales",
|
||||
Description = "Numero ordine vendita (es. ODV2025-00001)",
|
||||
SortOrder = 10,
|
||||
IsEnabled = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "invoice",
|
||||
EntityName = "Fattura",
|
||||
Prefix = "FT",
|
||||
Pattern = "{PREFIX}{YYYY}/{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "sales",
|
||||
Description = "Numero fattura (es. FT2025/00001)",
|
||||
SortOrder = 20,
|
||||
IsEnabled = false
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var config in defaults)
|
||||
{
|
||||
var exists = await _db.AutoCodes.AnyAsync(c => c.EntityCode == config.EntityCode);
|
||||
if (!exists)
|
||||
{
|
||||
config.CreatedAt = DateTime.Now;
|
||||
_db.AutoCodes.Add(config);
|
||||
_logger.LogInformation("Configurazione AutoCode creata per {EntityCode}", config.EntityCode);
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private bool ShouldResetSequence(AutoCode config, DateTime now)
|
||||
{
|
||||
if (config.ResetSequenceMonthly)
|
||||
{
|
||||
return config.LastResetYear != now.Year || config.LastResetMonth != now.Month;
|
||||
}
|
||||
|
||||
if (config.ResetSequenceYearly)
|
||||
{
|
||||
return config.LastResetYear != now.Year;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GenerateCodeFromPattern(string pattern, string? prefix, long sequence, DateTime date)
|
||||
{
|
||||
var code = pattern
|
||||
.Replace("{PREFIX}", prefix ?? "")
|
||||
.Replace("{YEAR}", date.Year.ToString())
|
||||
.Replace("{YYYY}", date.Year.ToString())
|
||||
.Replace("{YY}", date.Year.ToString().Substring(2))
|
||||
.Replace("{MONTH}", date.Month.ToString("D2"))
|
||||
.Replace("{MM}", date.Month.ToString("D2"))
|
||||
.Replace("{DAY}", date.Day.ToString("D2"))
|
||||
.Replace("{DD}", date.Day.ToString("D2"));
|
||||
|
||||
// Gestisci {SEQ:n}
|
||||
code = SequencePattern.Replace(code, match =>
|
||||
{
|
||||
var digits = int.Parse(match.Groups[1].Value);
|
||||
return sequence.ToString($"D{digits}");
|
||||
});
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private static void ValidatePattern(string pattern)
|
||||
{
|
||||
// Verifica che ci sia almeno un placeholder sequenza
|
||||
if (!SequencePattern.IsMatch(pattern))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Il pattern deve contenere almeno un placeholder {SEQ:n} per la sequenza numerica");
|
||||
}
|
||||
|
||||
// Verifica che i placeholder siano validi
|
||||
var validPlaceholders = new[]
|
||||
{
|
||||
"{PREFIX}", "{YEAR}", "{YYYY}", "{YY}",
|
||||
"{MONTH}", "{MM}", "{DAY}", "{DD}"
|
||||
};
|
||||
|
||||
var placeholderRegex = new Regex(@"\{[^}]+\}");
|
||||
var matches = placeholderRegex.Matches(pattern);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var placeholder = match.Value;
|
||||
if (!SequencePattern.IsMatch(placeholder) && !validPlaceholders.Contains(placeholder))
|
||||
{
|
||||
throw new ArgumentException($"Placeholder non valido: {placeholder}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO per l'aggiornamento di una configurazione AutoCode.
|
||||
/// </summary>
|
||||
public class AutoCodeUpdateDto
|
||||
{
|
||||
public string? Prefix { get; set; }
|
||||
public string? Pattern { get; set; }
|
||||
public bool? ResetSequenceYearly { get; set; }
|
||||
public bool? ResetSequenceMonthly { get; set; }
|
||||
public bool? IsEnabled { get; set; }
|
||||
public bool? IsReadOnly { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,16 @@ namespace Apollinare.Domain.Entities;
|
||||
|
||||
public class Articolo : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice articolo - generato automaticamente
|
||||
/// </summary>
|
||||
public string Codice { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? CodiceAlternativo { get; set; }
|
||||
|
||||
public string Descrizione { get; set; } = string.Empty;
|
||||
public int? TipoMaterialeId { get; set; }
|
||||
public int? CategoriaId { get; set; }
|
||||
|
||||
117
src/Apollinare.Domain/Entities/AutoCode.cs
Normal file
117
src/Apollinare.Domain/Entities/AutoCode.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
namespace Apollinare.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Configurazione per la generazione automatica di codici.
|
||||
/// Ogni entità può avere la propria configurazione con pattern personalizzabile.
|
||||
///
|
||||
/// Pattern supportati:
|
||||
/// - {SEQ:n} - Sequenza numerica con n cifre (es. {SEQ:5} → 00001)
|
||||
/// - {YEAR} o {YYYY} - Anno corrente a 4 cifre
|
||||
/// - {YY} - Anno corrente a 2 cifre
|
||||
/// - {MONTH} o {MM} - Mese corrente a 2 cifre
|
||||
/// - {DAY} o {DD} - Giorno corrente a 2 cifre
|
||||
/// - {PREFIX} - Usa il prefisso definito
|
||||
/// - Testo statico (es. "ART-", "-", "/")
|
||||
///
|
||||
/// Esempi di pattern:
|
||||
/// - "ART-{YYYY}-{SEQ:5}" → ART-2025-00001
|
||||
/// - "{PREFIX}{YY}{MM}{SEQ:4}" → MAG2511-0001
|
||||
/// - "CLI/{YYYY}/{SEQ:6}" → CLI/2025/000001
|
||||
/// </summary>
|
||||
public class AutoCode : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco dell'entità (es. "warehouse_article", "warehouse_location", "cliente")
|
||||
/// </summary>
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome visualizzato dell'entità (es. "Articolo Magazzino", "Magazzino", "Cliente")
|
||||
/// </summary>
|
||||
public string EntityName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Prefisso opzionale da usare nel pattern con {PREFIX}
|
||||
/// </summary>
|
||||
public string? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pattern per la generazione del codice
|
||||
/// </summary>
|
||||
public string Pattern { get; set; } = "{PREFIX}{SEQ:5}";
|
||||
|
||||
/// <summary>
|
||||
/// Ultimo numero di sequenza utilizzato (per {SEQ:n})
|
||||
/// </summary>
|
||||
public long LastSequence { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la sequenza viene resettata ogni anno
|
||||
/// </summary>
|
||||
public bool ResetSequenceYearly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la sequenza viene resettata ogni mese
|
||||
/// </summary>
|
||||
public bool ResetSequenceMonthly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Anno dell'ultimo reset della sequenza
|
||||
/// </summary>
|
||||
public int? LastResetYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mese dell'ultimo reset della sequenza (se ResetSequenceMonthly)
|
||||
/// </summary>
|
||||
public int? LastResetMonth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la generazione automatica è abilitata
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, il codice non può essere modificato manualmente
|
||||
/// </summary>
|
||||
public bool IsReadOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Modulo di appartenenza (es. "core", "warehouse", "purchases")
|
||||
/// </summary>
|
||||
public string? ModuleCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Descrizione della configurazione
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordine di visualizzazione nel pannello admin
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Esempio di codice generato (calcolato, non persistito)
|
||||
/// </summary>
|
||||
public string GetExampleCode()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
return Pattern
|
||||
.Replace("{PREFIX}", Prefix ?? "")
|
||||
.Replace("{YEAR}", now.Year.ToString())
|
||||
.Replace("{YYYY}", now.Year.ToString())
|
||||
.Replace("{YY}", now.Year.ToString().Substring(2))
|
||||
.Replace("{MONTH}", now.Month.ToString("D2"))
|
||||
.Replace("{MM}", now.Month.ToString("D2"))
|
||||
.Replace("{DAY}", now.Day.ToString("D2"))
|
||||
.Replace("{DD}", now.Day.ToString("D2"))
|
||||
.Replace("{SEQ:1}", "X")
|
||||
.Replace("{SEQ:2}", "XX")
|
||||
.Replace("{SEQ:3}", "XXX")
|
||||
.Replace("{SEQ:4}", "XXXX")
|
||||
.Replace("{SEQ:5}", "XXXXX")
|
||||
.Replace("{SEQ:6}", "XXXXXX")
|
||||
.Replace("{SEQ:7}", "XXXXXXX")
|
||||
.Replace("{SEQ:8}", "XXXXXXXX");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,16 @@ namespace Apollinare.Domain.Entities;
|
||||
|
||||
public class Cliente : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice cliente - generato automaticamente
|
||||
/// </summary>
|
||||
public string Codice { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? CodiceAlternativo { get; set; }
|
||||
|
||||
public string RagioneSociale { get; set; } = string.Empty;
|
||||
public string? Indirizzo { get; set; }
|
||||
public string? Cap { get; set; }
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseArticle : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco articolo (SKU)
|
||||
/// Codice univoco articolo (SKU) - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Descrizione articolo
|
||||
/// </summary>
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseArticleCategory : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice categoria
|
||||
/// Codice categoria - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nome categoria
|
||||
/// </summary>
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseLocation : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco del magazzino (es. "MAG01", "CENTRALE")
|
||||
/// Codice univoco del magazzino - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nome descrittivo del magazzino
|
||||
/// </summary>
|
||||
|
||||
@@ -41,6 +41,9 @@ public class AppollinareDbContext : DbContext
|
||||
public DbSet<AppModule> AppModules => Set<AppModule>();
|
||||
public DbSet<ModuleSubscription> ModuleSubscriptions => Set<ModuleSubscription>();
|
||||
|
||||
// Auto Code system
|
||||
public DbSet<AutoCode> AutoCodes => Set<AutoCode>();
|
||||
|
||||
// Warehouse module entities
|
||||
public DbSet<WarehouseLocation> WarehouseLocations => Set<WarehouseLocation>();
|
||||
public DbSet<WarehouseArticle> WarehouseArticles => Set<WarehouseArticle>();
|
||||
@@ -274,6 +277,13 @@ public class AppollinareDbContext : DbContext
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// AutoCode
|
||||
modelBuilder.Entity<AutoCode>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.EntityCode).IsUnique();
|
||||
entity.HasIndex(e => e.ModuleCode);
|
||||
});
|
||||
|
||||
// ===============================================
|
||||
// WAREHOUSE MODULE ENTITIES
|
||||
// ===============================================
|
||||
|
||||
3091
src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
generated
Normal file
3091
src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Apollinare.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAutoCodeSystem : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AutoCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
EntityCode = table.Column<string>(type: "TEXT", nullable: false),
|
||||
EntityName = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Prefix = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Pattern = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LastSequence = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
ResetSequenceYearly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ResetSequenceMonthly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LastResetYear = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
LastResetMonth = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IsReadOnly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ModuleCode = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AutoCodes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoCodes_EntityCode",
|
||||
table: "AutoCodes",
|
||||
column: "EntityCode",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoCodes_ModuleCode",
|
||||
table: "AutoCodes",
|
||||
column: "ModuleCode");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AutoCodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
3110
src/Apollinare.Infrastructure/Migrations/20251129144249_AddAlternativeCodeFields.Designer.cs
generated
Normal file
3110
src/Apollinare.Infrastructure/Migrations/20251129144249_AddAlternativeCodeFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Apollinare.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlternativeCodeFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseLocations",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticles",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticleCategories",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Codice",
|
||||
table: "Clienti",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Clienti",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Articoli",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseLocations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticleCategories");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Codice",
|
||||
table: "Clienti");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Clienti");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Articoli");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceAlternativo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -153,6 +156,79 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.ToTable("Articoli");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Apollinare.Domain.Entities.AutoCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EntityCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EntityName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsReadOnly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LastResetMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LastResetYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("LastSequence")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ModuleCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Pattern")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Prefix")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ResetSequenceMonthly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ResetSequenceYearly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EntityCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ModuleCode");
|
||||
|
||||
b.ToTable("AutoCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Apollinare.Domain.Entities.Cliente", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -168,6 +244,13 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.Property<string>("Citta")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Codice")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceAlternativo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceDestinatario")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -2170,6 +2253,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Barcode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -2315,6 +2401,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -2386,6 +2475,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user