diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bb7c2cb..4c9de96 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -20,6 +20,7 @@ import ReportEditorPage from "./pages/ReportEditorPage";
import ModulesAdminPage from "./pages/ModulesAdminPage";
import ModulePurchasePage from "./pages/ModulePurchasePage";
import AutoCodesAdminPage from "./pages/AutoCodesAdminPage";
+import CustomFieldsAdminPage from "./pages/CustomFieldsAdminPage";
import WarehouseRoutes from "./modules/warehouse/routes";
import { ModuleGuard } from "./components/ModuleGuard";
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
@@ -101,6 +102,10 @@ function App() {
path="admin/auto-codes"
element={}
/>
+ }
+ />
{/* Warehouse Module */}
void;
+ readOnly?: boolean;
+}
+
+export const CustomFieldsRenderer: React.FC = ({ entityName, values, onChange, readOnly = false }) => {
+ const [definitions, setDefinitions] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadDefinitions = async () => {
+ try {
+ const defs = await customFieldService.getByEntity(entityName);
+ setDefinitions(defs);
+ } catch (error) {
+ console.error("Failed to load custom fields", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadDefinitions();
+ }, [entityName]);
+
+ if (loading) return null;
+ if (definitions.length === 0) return null;
+
+ return (
+
+
+ Campi Personalizzati
+
+
+ {definitions.map(def => (
+
+ onChange(def.fieldName, val)}
+ readOnly={readOnly}
+ />
+
+ ))}
+
+
+ );
+};
+
+const FieldRenderer: React.FC<{
+ definition: CustomFieldDefinition;
+ value: any;
+ onChange: (value: any) => void;
+ readOnly: boolean;
+}> = ({ definition, value, onChange, readOnly }) => {
+
+ const handleChange = (e: React.ChangeEvent) => {
+ onChange(e.target.value);
+ };
+
+ switch (definition.type) {
+ case CustomFieldType.Text:
+ case CustomFieldType.Email:
+ case CustomFieldType.Url:
+ return (
+
+ );
+
+ case CustomFieldType.Number:
+ return (
+
+ );
+
+ case CustomFieldType.TextArea:
+ return (
+
+ );
+
+ case CustomFieldType.Boolean:
+ return (
+ onChange(e.target.checked)}
+ disabled={readOnly}
+ />
+ }
+ label={definition.label}
+ />
+ );
+
+ case CustomFieldType.Date:
+ return (
+ onChange(newValue ? newValue.toISOString() : null)}
+ disabled={readOnly}
+ slotProps={{ textField: { fullWidth: true, required: definition.isRequired, helperText: definition.description, size: 'small' } }}
+ />
+ );
+
+ case CustomFieldType.Select:
+ let options: string[] = [];
+ try {
+ options = definition.optionsJson ? JSON.parse(definition.optionsJson) : [];
+ } catch (e) {
+ console.error("Invalid options JSON", e);
+ }
+ return (
+
+ {options.map((opt) => (
+
+ ))}
+
+ );
+
+ case CustomFieldType.Color:
+ return (
+
+ );
+
+ default:
+ return null;
+ }
+};
diff --git a/frontend/src/pages/ClientiPage.tsx b/frontend/src/pages/ClientiPage.tsx
index 0f9e92f..19a26de 100644
--- a/frontend/src/pages/ClientiPage.tsx
+++ b/frontend/src/pages/ClientiPage.tsx
@@ -11,7 +11,7 @@ import {
DialogContent,
DialogActions,
TextField,
- Grid,
+
} from "@mui/material";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import {
@@ -21,12 +21,15 @@ import {
} from "@mui/icons-material";
import { clientiService } from "../services/lookupService";
import { Cliente } from "../types";
+import { CustomFieldsRenderer } from "../components/customFields/CustomFieldsRenderer";
+import { CustomFieldValues } from "../types/customFields";
export default function ClientiPage() {
const queryClient = useQueryClient();
const [openDialog, setOpenDialog] = useState(false);
const [editingId, setEditingId] = useState(null);
const [formData, setFormData] = useState>({ attivo: true });
+ const [customFields, setCustomFields] = useState({});
const { data: clienti = [], isLoading } = useQuery({
queryKey: ["clienti"],
@@ -59,22 +62,33 @@ export default function ClientiPage() {
setOpenDialog(false);
setEditingId(null);
setFormData({ attivo: true });
+ setCustomFields({});
};
const handleEdit = (cliente: Cliente) => {
setFormData(cliente);
setEditingId(cliente.id);
+ try {
+ setCustomFields(cliente.customFieldsJson ? JSON.parse(cliente.customFieldsJson) : {});
+ } catch (e) {
+ setCustomFields({});
+ }
setOpenDialog(true);
};
const handleSubmit = () => {
+ const dataWithCustomFields = {
+ ...formData,
+ customFieldsJson: JSON.stringify(customFields)
+ };
+
if (editingId) {
// In modifica, non inviamo il codice (non modificabile)
- const { codice: _codice, ...updateData } = formData;
+ const { codice: _codice, ...updateData } = dataWithCustomFields;
updateMutation.mutate({ id: editingId, data: updateData });
} else {
// In creazione, non inviamo il codice (generato automaticamente)
- const { codice: _codice, ...createData } = formData;
+ const { codice: _codice, ...createData } = dataWithCustomFields;
createMutation.mutate(createData);
}
};
@@ -162,8 +176,8 @@ export default function ClientiPage() {
{editingId ? "Modifica Cliente" : "Nuovo Cliente"}
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+ setCustomFields(prev => ({ ...prev, [field]: value }))}
+ />
+
+
diff --git a/frontend/src/pages/CustomFieldsAdminPage.tsx b/frontend/src/pages/CustomFieldsAdminPage.tsx
new file mode 100644
index 0000000..84f3061
--- /dev/null
+++ b/frontend/src/pages/CustomFieldsAdminPage.tsx
@@ -0,0 +1,302 @@
+
+import React, { useState, useEffect } from 'react';
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControl,
+
+ InputLabel,
+ MenuItem,
+ Select,
+ TextField,
+ Typography,
+ FormControlLabel,
+ Checkbox,
+ Snackbar,
+ Alert
+} from '@mui/material';
+import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid';
+import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
+import { CustomFieldDefinition, CustomFieldType } from '../types/customFields';
+import { customFieldService } from '../services/customFieldService';
+
+const ENTITIES = [
+ { value: 'Cliente', label: 'Clienti' },
+ { value: 'Articolo', label: 'Articoli (Catering)' },
+ { value: 'Evento', label: 'Eventi' },
+ { value: 'WarehouseArticle', label: 'Articoli Magazzino' },
+ { value: 'WarehouseLocation', label: 'Magazzini' },
+ { value: 'Risorsa', label: 'Risorse (Staff)' }
+];
+
+const FIELD_TYPES = [
+ { value: CustomFieldType.Text, label: 'Testo' },
+ { value: CustomFieldType.Number, label: 'Numero' },
+ { value: CustomFieldType.Date, label: 'Data' },
+ { value: CustomFieldType.Boolean, label: 'Booleano (Sì/No)' },
+ { value: CustomFieldType.Select, label: 'Lista a discesa' },
+ { value: CustomFieldType.TextArea, label: 'Area di testo' },
+ { value: CustomFieldType.Color, label: 'Colore' },
+ { value: CustomFieldType.Url, label: 'URL' },
+ { value: CustomFieldType.Email, label: 'Email' }
+];
+
+const CustomFieldsAdminPage: React.FC = () => {
+ const [selectedEntity, setSelectedEntity] = useState(ENTITIES[0].value);
+ const [fields, setFields] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [currentField, setCurrentField] = useState>({});
+ const [snackbar, setSnackbar] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
+ open: false,
+ message: '',
+ severity: 'success'
+ });
+
+ const showSnackbar = (message: string, severity: 'success' | 'error') => {
+ setSnackbar({ open: true, message, severity });
+ };
+
+ const handleCloseSnackbar = () => setSnackbar({ ...snackbar, open: false });
+
+ const loadFields = async () => {
+ setLoading(true);
+ try {
+ const data = await customFieldService.getByEntity(selectedEntity);
+ setFields(data);
+ } catch (error) {
+ showSnackbar('Errore nel caricamento dei campi', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadFields();
+ }, [selectedEntity]);
+
+ const handleSave = async () => {
+ try {
+ if (currentField.id) {
+ await customFieldService.update(currentField.id, currentField as CustomFieldDefinition);
+ showSnackbar('Campo aggiornato con successo', 'success');
+ } else {
+ await customFieldService.create({
+ ...currentField,
+ entityName: selectedEntity,
+ isActive: true,
+ sortOrder: fields.length + 1
+ } as CustomFieldDefinition);
+ showSnackbar('Campo creato con successo', 'success');
+ }
+ setDialogOpen(false);
+ loadFields();
+ } catch (error) {
+ showSnackbar('Errore nel salvataggio', 'error');
+ }
+ };
+
+ const handleDelete = async (id: number) => {
+ if (window.confirm('Sei sicuro di voler eliminare questo campo?')) {
+ try {
+ await customFieldService.delete(id);
+ showSnackbar('Campo eliminato', 'success');
+ loadFields();
+ } catch (error) {
+ showSnackbar('Errore durante l\'eliminazione', 'error');
+ }
+ }
+ };
+
+ const columns: GridColDef[] = [
+ { field: 'label', headerName: 'Etichetta', flex: 1 },
+ { field: 'fieldName', headerName: 'Nome Interno', flex: 1 },
+ {
+ field: 'type',
+ headerName: 'Tipo',
+ width: 150,
+ valueFormatter: (params) => FIELD_TYPES.find(t => t.value === params.value)?.label
+ },
+ {
+ field: 'isRequired',
+ headerName: 'Obbligatorio',
+ width: 120,
+ type: 'boolean'
+ },
+ {
+ field: 'sortOrder',
+ headerName: 'Ordine',
+ width: 100,
+ type: 'number',
+ editable: true
+ },
+ {
+ field: 'actions',
+ type: 'actions',
+ headerName: 'Azioni',
+ width: 100,
+ getActions: (params) => [
+ }
+ label="Modifica"
+ onClick={() => {
+ setCurrentField(params.row);
+ setDialogOpen(true);
+ }}
+ />,
+ }
+ label="Elimina"
+ onClick={() => handleDelete(params.row.id)}
+ />,
+ ],
+ },
+ ];
+
+ return (
+
+
+ Gestione Campi Personalizzati
+
+
+
+
+
+
+
+ Entità
+
+
+
+
+ }
+ onClick={() => {
+ setCurrentField({
+ type: CustomFieldType.Text,
+ isRequired: false,
+ entityName: selectedEntity
+ });
+ setDialogOpen(true);
+ }}
+ >
+ Nuovo Campo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {snackbar.message}
+
+
+
+ );
+};
+
+export default CustomFieldsAdminPage;
+
diff --git a/frontend/src/services/customFieldService.ts b/frontend/src/services/customFieldService.ts
new file mode 100644
index 0000000..3170c3b
--- /dev/null
+++ b/frontend/src/services/customFieldService.ts
@@ -0,0 +1,24 @@
+import api from './api';
+import { CustomFieldDefinition } from '../types/customFields';
+
+export const customFieldService = {
+ getAll: async () => {
+ const response = await api.get('/custom-fields');
+ return response.data;
+ },
+ getByEntity: async (entityName: string) => {
+ const response = await api.get(`/custom-fields/entity/${entityName}`);
+ return response.data;
+ },
+ create: async (definition: Omit) => {
+ const response = await api.post('/custom-fields', definition);
+ return response.data;
+ },
+ update: async (id: number, definition: CustomFieldDefinition) => {
+ const response = await api.put(`/custom-fields/${id}`, definition);
+ return response.data;
+ },
+ delete: async (id: number) => {
+ await api.delete(`/custom-fields/${id}`);
+ }
+};
diff --git a/frontend/src/types/customFields.ts b/frontend/src/types/customFields.ts
new file mode 100644
index 0000000..435dee5
--- /dev/null
+++ b/frontend/src/types/customFields.ts
@@ -0,0 +1,32 @@
+export enum CustomFieldType {
+ Text = 0,
+ Number = 1,
+ Date = 2,
+ Boolean = 3,
+ Select = 4,
+ MultiSelect = 5,
+ TextArea = 6,
+ Color = 7,
+ Url = 8,
+ Email = 9
+}
+
+export interface CustomFieldDefinition {
+ id: number;
+ entityName: string;
+ fieldName: string;
+ label: string;
+ type: CustomFieldType;
+ isRequired: boolean;
+ defaultValue?: string;
+ optionsJson?: string;
+ sortOrder: number;
+ description?: string;
+ isActive: boolean;
+ validationRegex?: string;
+ placeholder?: string;
+}
+
+export interface CustomFieldValues {
+ [key: string]: any;
+}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 939c149..c37e6ae 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -10,6 +10,7 @@ export interface BaseEntity {
createdBy?: string;
updatedAt?: string;
updatedBy?: string;
+ customFieldsJson?: string;
}
export interface Cliente extends BaseEntity {
diff --git a/src/Apollinare.API/Controllers/CustomFieldDefinitionsController.cs b/src/Apollinare.API/Controllers/CustomFieldDefinitionsController.cs
new file mode 100644
index 0000000..fbc9420
--- /dev/null
+++ b/src/Apollinare.API/Controllers/CustomFieldDefinitionsController.cs
@@ -0,0 +1,153 @@
+using Apollinare.API.Services;
+using Apollinare.Domain.Entities;
+using Apollinare.Domain.Enums;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Apollinare.API.Controllers;
+
+[ApiController]
+[Route("api/custom-fields")]
+public class CustomFieldDefinitionsController : ControllerBase
+{
+ private readonly CustomFieldService _service;
+ private readonly ILogger _logger;
+
+ public CustomFieldDefinitionsController(CustomFieldService service, ILogger logger)
+ {
+ _service = service;
+ _logger = logger;
+ }
+
+ [HttpGet]
+ public async Task>> GetAll()
+ {
+ var defs = await _service.GetAllDefinitionsAsync();
+ return Ok(defs.Select(ToDto).ToList());
+ }
+
+ [HttpGet("entity/{entityName}")]
+ public async Task>> GetByEntity(string entityName)
+ {
+ var defs = await _service.GetDefinitionsByEntityAsync(entityName);
+ return Ok(defs.Select(ToDto).ToList());
+ }
+
+ [HttpGet("{id:int}")]
+ public async Task> Get(int id)
+ {
+ var def = await _service.GetDefinitionAsync(id);
+ if (def == null) return NotFound();
+ return Ok(ToDto(def));
+ }
+
+ [HttpPost]
+ public async Task> Create(CustomFieldDefinitionDto dto)
+ {
+ try
+ {
+ var entity = new CustomFieldDefinition
+ {
+ EntityName = dto.EntityName,
+ FieldName = dto.FieldName,
+ Label = dto.Label,
+ Type = dto.Type,
+ IsRequired = dto.IsRequired,
+ DefaultValue = dto.DefaultValue,
+ OptionsJson = dto.OptionsJson,
+ SortOrder = dto.SortOrder,
+ Description = dto.Description,
+ IsActive = dto.IsActive,
+ ValidationRegex = dto.ValidationRegex,
+ Placeholder = dto.Placeholder
+ };
+
+ var created = await _service.CreateDefinitionAsync(entity);
+ return CreatedAtAction(nameof(Get), new { id = created.Id }, ToDto(created));
+ }
+ catch (ArgumentException ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
+ }
+
+ [HttpPut("{id:int}")]
+ public async Task> Update(int id, CustomFieldDefinitionDto dto)
+ {
+ try
+ {
+ var entity = new CustomFieldDefinition
+ {
+ EntityName = dto.EntityName,
+ FieldName = dto.FieldName,
+ Label = dto.Label,
+ Type = dto.Type,
+ IsRequired = dto.IsRequired,
+ DefaultValue = dto.DefaultValue,
+ OptionsJson = dto.OptionsJson,
+ SortOrder = dto.SortOrder,
+ Description = dto.Description,
+ IsActive = dto.IsActive,
+ ValidationRegex = dto.ValidationRegex,
+ Placeholder = dto.Placeholder
+ };
+
+ var updated = await _service.UpdateDefinitionAsync(id, entity);
+ return Ok(ToDto(updated));
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpDelete("{id:int}")]
+ public async Task Delete(int id)
+ {
+ try
+ {
+ await _service.DeleteDefinitionAsync(id);
+ return NoContent();
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ private static CustomFieldDefinitionDto ToDto(CustomFieldDefinition entity)
+ {
+ return new CustomFieldDefinitionDto
+ {
+ Id = entity.Id,
+ EntityName = entity.EntityName,
+ FieldName = entity.FieldName,
+ Label = entity.Label,
+ Type = entity.Type,
+ IsRequired = entity.IsRequired,
+ DefaultValue = entity.DefaultValue,
+ OptionsJson = entity.OptionsJson,
+ SortOrder = entity.SortOrder,
+ Description = entity.Description,
+ IsActive = entity.IsActive,
+ ValidationRegex = entity.ValidationRegex,
+ Placeholder = entity.Placeholder
+ };
+ }
+}
+
+public class CustomFieldDefinitionDto
+{
+ public int Id { get; set; }
+ public required string EntityName { get; set; }
+ public required string FieldName { get; set; }
+ public required string Label { get; set; }
+ public CustomFieldType Type { get; set; }
+ public bool IsRequired { get; set; }
+ public string? DefaultValue { get; set; }
+ public string? OptionsJson { get; set; }
+ public int SortOrder { get; set; }
+ public string? Description { get; set; }
+ public bool IsActive { get; set; }
+ public string? ValidationRegex { get; set; }
+ public string? Placeholder { get; set; }
+}
diff --git a/src/Apollinare.API/Program.cs b/src/Apollinare.API/Program.cs
index c6478dd..7fa626d 100644
--- a/src/Apollinare.API/Program.cs
+++ b/src/Apollinare.API/Program.cs
@@ -1,5 +1,6 @@
using Apollinare.API.Hubs;
using Apollinare.API.Services;
+// Trigger rebuild
using Apollinare.API.Services.Reports;
using Apollinare.API.Modules.Warehouse.Services;
using Apollinare.Infrastructure.Data;
@@ -20,6 +21,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddSingleton();
// Warehouse Module Services
diff --git a/src/Apollinare.API/Services/CustomFieldService.cs b/src/Apollinare.API/Services/CustomFieldService.cs
new file mode 100644
index 0000000..da4767b
--- /dev/null
+++ b/src/Apollinare.API/Services/CustomFieldService.cs
@@ -0,0 +1,91 @@
+using Apollinare.Domain.Entities;
+using Apollinare.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace Apollinare.API.Services;
+
+public class CustomFieldService
+{
+ private readonly AppollinareDbContext _context;
+ private readonly ILogger _logger;
+
+ public CustomFieldService(AppollinareDbContext context, ILogger logger)
+ {
+ _context = context;
+ _logger = logger;
+ }
+
+ public async Task> GetAllDefinitionsAsync()
+ {
+ return await _context.CustomFieldDefinitions
+ .OrderBy(x => x.EntityName)
+ .ThenBy(x => x.SortOrder)
+ .ToListAsync();
+ }
+
+ public async Task> GetDefinitionsByEntityAsync(string entityName)
+ {
+ return await _context.CustomFieldDefinitions
+ .Where(x => x.EntityName == entityName && x.IsActive)
+ .OrderBy(x => x.SortOrder)
+ .ToListAsync();
+ }
+
+ public async Task GetDefinitionAsync(int id)
+ {
+ return await _context.CustomFieldDefinitions.FindAsync(id);
+ }
+
+ public async Task CreateDefinitionAsync(CustomFieldDefinition definition)
+ {
+ // Check for duplicate field name in the same entity
+ var exists = await _context.CustomFieldDefinitions
+ .AnyAsync(x => x.EntityName == definition.EntityName && x.FieldName == definition.FieldName);
+
+ if (exists)
+ {
+ throw new ArgumentException($"Field '{definition.FieldName}' already exists for entity '{definition.EntityName}'");
+ }
+
+ definition.CreatedAt = DateTime.UtcNow;
+ _context.CustomFieldDefinitions.Add(definition);
+ await _context.SaveChangesAsync();
+ return definition;
+ }
+
+ public async Task UpdateDefinitionAsync(int id, CustomFieldDefinition updatedDef)
+ {
+ var existing = await _context.CustomFieldDefinitions.FindAsync(id);
+ if (existing == null)
+ {
+ throw new KeyNotFoundException($"CustomFieldDefinition with ID {id} not found");
+ }
+
+ existing.Label = updatedDef.Label;
+ existing.Type = updatedDef.Type;
+ existing.IsRequired = updatedDef.IsRequired;
+ existing.DefaultValue = updatedDef.DefaultValue;
+ existing.OptionsJson = updatedDef.OptionsJson;
+ existing.SortOrder = updatedDef.SortOrder;
+ existing.Description = updatedDef.Description;
+ existing.IsActive = updatedDef.IsActive;
+ existing.ValidationRegex = updatedDef.ValidationRegex;
+ existing.Placeholder = updatedDef.Placeholder;
+ existing.UpdatedAt = DateTime.UtcNow;
+
+ await _context.SaveChangesAsync();
+ return existing;
+ }
+
+ public async Task DeleteDefinitionAsync(int id)
+ {
+ var existing = await _context.CustomFieldDefinitions.FindAsync(id);
+ if (existing == null)
+ {
+ throw new KeyNotFoundException($"CustomFieldDefinition with ID {id} not found");
+ }
+
+ _context.CustomFieldDefinitions.Remove(existing);
+ await _context.SaveChangesAsync();
+ }
+}
diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm
index fc0c255..99ddfc3 100644
Binary files a/src/Apollinare.API/apollinare.db-shm and b/src/Apollinare.API/apollinare.db-shm differ
diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal
index 7a1b575..856b57f 100644
Binary files a/src/Apollinare.API/apollinare.db-wal and b/src/Apollinare.API/apollinare.db-wal differ
diff --git a/src/Apollinare.Domain/Entities/BaseEntity.cs b/src/Apollinare.Domain/Entities/BaseEntity.cs
index 3e78ada..00a917c 100644
--- a/src/Apollinare.Domain/Entities/BaseEntity.cs
+++ b/src/Apollinare.Domain/Entities/BaseEntity.cs
@@ -7,4 +7,9 @@ public abstract class BaseEntity
public string? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
+
+ ///
+ /// Stores custom field values as JSON: {"birthday": "2023-01-01", "vip_status": "gold"}
+ ///
+ public string? CustomFieldsJson { get; set; }
}
diff --git a/src/Apollinare.Domain/Entities/CustomFieldDefinition.cs b/src/Apollinare.Domain/Entities/CustomFieldDefinition.cs
new file mode 100644
index 0000000..e2aa4e9
--- /dev/null
+++ b/src/Apollinare.Domain/Entities/CustomFieldDefinition.cs
@@ -0,0 +1,19 @@
+using Apollinare.Domain.Enums;
+
+namespace Apollinare.Domain.Entities;
+
+public class CustomFieldDefinition : BaseEntity
+{
+ public required string EntityName { get; set; } // e.g. "Cliente", "WarehouseArticle"
+ public required string FieldName { get; set; } // Internal name, e.g. "birthday"
+ public required string Label { get; set; } // Display name, e.g. "Birthday"
+ public CustomFieldType Type { get; set; }
+ public bool IsRequired { get; set; }
+ public string? DefaultValue { get; set; }
+ public string? OptionsJson { get; set; } // For Select/MultiSelect: ["Option A", "Option B"]
+ public int SortOrder { get; set; }
+ public string? Description { get; set; }
+ public bool IsActive { get; set; } = true;
+ public string? ValidationRegex { get; set; }
+ public string? Placeholder { get; set; }
+}
diff --git a/src/Apollinare.Domain/Enums/CustomFieldType.cs b/src/Apollinare.Domain/Enums/CustomFieldType.cs
new file mode 100644
index 0000000..7db8ea7
--- /dev/null
+++ b/src/Apollinare.Domain/Enums/CustomFieldType.cs
@@ -0,0 +1,15 @@
+namespace Apollinare.Domain.Enums;
+
+public enum CustomFieldType
+{
+ Text = 0,
+ Number = 1,
+ Date = 2,
+ Boolean = 3,
+ Select = 4,
+ MultiSelect = 5,
+ TextArea = 6,
+ Color = 7,
+ Url = 8,
+ Email = 9
+}
diff --git a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
index 0c49e4c..c11d2a9 100644
--- a/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
+++ b/src/Apollinare.Infrastructure/Data/AppollinareDbContext.cs
@@ -44,6 +44,9 @@ public class AppollinareDbContext : DbContext
// Auto Code system
public DbSet AutoCodes => Set();
+ // Custom Fields system
+ public DbSet CustomFieldDefinitions => Set();
+
// Warehouse module entities
public DbSet WarehouseLocations => Set();
public DbSet WarehouseArticles => Set();
@@ -284,6 +287,13 @@ public class AppollinareDbContext : DbContext
entity.HasIndex(e => e.ModuleCode);
});
+ // CustomFieldDefinition
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => new { e.EntityName, e.FieldName }).IsUnique();
+ entity.HasIndex(e => e.EntityName);
+ });
+
// ===============================================
// WAREHOUSE MODULE ENTITIES
// ===============================================
diff --git a/src/Apollinare.Infrastructure/Migrations/20251129161359_AddCustomFieldsSystem.Designer.cs b/src/Apollinare.Infrastructure/Migrations/20251129161359_AddCustomFieldsSystem.Designer.cs
new file mode 100644
index 0000000..6fc86ed
--- /dev/null
+++ b/src/Apollinare.Infrastructure/Migrations/20251129161359_AddCustomFieldsSystem.Designer.cs
@@ -0,0 +1,3303 @@
+//
+using System;
+using Apollinare.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Apollinare.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppollinareDbContext))]
+ [Migration("20251129161359_AddCustomFieldsSystem")]
+ partial class AddCustomFieldsSystem
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.AppModule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("BasePrice")
+ .HasPrecision(18, 2)
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Dependencies")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("Icon")
+ .HasColumnType("TEXT");
+
+ b.Property("IsAvailable")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsCore")
+ .HasColumnType("INTEGER");
+
+ b.Property("MonthlyMultiplier")
+ .HasPrecision(5, 2)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("RoutePath")
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("SortOrder");
+
+ b.ToTable("AppModules");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Articolo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("CategoriaId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceAlternativo")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Immagine")
+ .HasColumnType("BLOB");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaDisponibile")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdA")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdB")
+ .HasColumnType("TEXT");
+
+ b.Property("QtaStdS")
+ .HasColumnType("TEXT");
+
+ b.Property("TipoMaterialeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("UnitaMisura")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoriaId");
+
+ b.HasIndex("Codice")
+ .IsUnique();
+
+ b.HasIndex("TipoMaterialeId");
+
+ b.ToTable("Articoli");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.AutoCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityCode")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("EntityName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsReadOnly")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastResetMonth")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastResetYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastSequence")
+ .HasColumnType("INTEGER");
+
+ b.Property("ModuleCode")
+ .HasColumnType("TEXT");
+
+ b.Property("Pattern")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Prefix")
+ .HasColumnType("TEXT");
+
+ b.Property("ResetSequenceMonthly")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResetSequenceYearly")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("Cap")
+ .HasColumnType("TEXT");
+
+ b.Property("Citta")
+ .HasColumnType("TEXT");
+
+ b.Property("Codice")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceAlternativo")
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceDestinatario")
+ .HasColumnType("TEXT");
+
+ b.Property("CodiceFiscale")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("Indirizzo")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("PartitaIva")
+ .HasColumnType("TEXT");
+
+ b.Property("Pec")
+ .HasColumnType("TEXT");
+
+ b.Property("Provincia")
+ .HasColumnType("TEXT");
+
+ b.Property("RagioneSociale")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Telefono")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PartitaIva");
+
+ b.HasIndex("RagioneSociale");
+
+ b.ToTable("Clienti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.CodiceCategoria", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Attivo")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffA")
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffB")
+ .HasColumnType("TEXT");
+
+ b.Property("CoeffS")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("CodiciCategoria");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Configurazione", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Chiave")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Valore")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Chiave")
+ .IsUnique();
+
+ b.ToTable("Configurazioni");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.CustomFieldDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("DefaultValue")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasColumnType("TEXT");
+
+ b.Property("EntityName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FieldName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsRequired")
+ .HasColumnType("INTEGER");
+
+ b.Property("Label")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("OptionsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Placeholder")
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("ValidationRegex")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EntityName");
+
+ b.HasIndex("EntityName", "FieldName")
+ .IsUnique();
+
+ b.ToTable("CustomFieldDefinitions");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.Evento", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClienteId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codice")
+ .HasColumnType("TEXT");
+
+ b.Property("Confermato")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoPersona")
+ .HasColumnType("TEXT");
+
+ b.Property("CostoTotale")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("DataEvento")
+ .HasColumnType("TEXT");
+
+ b.Property("DataScadenzaPreventivo")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("LocationId")
+ .HasColumnType("INTEGER");
+
+ b.Property("NoteAllestimento")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteCliente")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteCucina")
+ .HasColumnType("TEXT");
+
+ b.Property("NoteInterne")
+ .HasColumnType("TEXT");
+
+ b.Property("NumeroOspiti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiAdulti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiBambini")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiBuffet")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroOspitiSeduti")
+ .HasColumnType("INTEGER");
+
+ b.Property("OraFine")
+ .HasColumnType("TEXT");
+
+ b.Property("OraInizio")
+ .HasColumnType("TEXT");
+
+ b.Property("Saldo")
+ .HasColumnType("TEXT");
+
+ b.Property("Stato")
+ .HasColumnType("INTEGER");
+
+ b.Property("TipoEventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TotaleAcconti")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClienteId");
+
+ b.HasIndex("Codice");
+
+ b.HasIndex("DataEvento");
+
+ b.HasIndex("LocationId");
+
+ b.HasIndex("Stato");
+
+ b.HasIndex("TipoEventoId");
+
+ b.ToTable("Eventi");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAcconto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AConferma")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("DataPagamento")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Importo")
+ .HasColumnType("TEXT");
+
+ b.Property("MetodoPagamento")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("Ordine")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAcconti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAllegato", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Contenuto")
+ .HasColumnType("BLOB");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.Property("NomeFile")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAllegati");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoAltroCosto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AliquotaIva")
+ .HasColumnType("TEXT");
+
+ b.Property("ApplicaIva")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoUnitario")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("Descrizione")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Ordine")
+ .HasColumnType("INTEGER");
+
+ b.Property("Quantita")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiAltriCosti");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoDegustazione", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Completata")
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoDegustazione")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("CustomFieldsJson")
+ .HasColumnType("TEXT");
+
+ b.Property("DataDegustazione")
+ .HasColumnType("TEXT");
+
+ b.Property("Detraibile")
+ .HasColumnType("INTEGER");
+
+ b.Property("EventoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Luogo")
+ .HasColumnType("TEXT");
+
+ b.Property("Menu")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasColumnType("TEXT");
+
+ b.Property("NumeroPaganti")
+ .HasColumnType("INTEGER");
+
+ b.Property("NumeroPersone")
+ .HasColumnType("INTEGER");
+
+ b.Property("Ora")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventoId");
+
+ b.ToTable("EventiDegustazioni");
+ });
+
+ modelBuilder.Entity("Apollinare.Domain.Entities.EventoDettaglioOspiti", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CostoUnitario")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property