feat: Introduce dynamic tab management, sidebar, and search bar components, update backend database schema, and remove old SQL schema.
This commit is contained in:
@@ -1,421 +0,0 @@
|
||||
-- =====================================================
|
||||
-- APOLLINARE WAREHOUSE MODULE - DATABASE TABLES
|
||||
-- SQLite
|
||||
-- =====================================================
|
||||
|
||||
-- Magazzini
|
||||
CREATE TABLE IF NOT EXISTS WarehouseLocations (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL UNIQUE,
|
||||
Name TEXT NOT NULL,
|
||||
Description TEXT,
|
||||
Address TEXT,
|
||||
City TEXT,
|
||||
Province TEXT,
|
||||
PostalCode TEXT,
|
||||
Country TEXT DEFAULT 'Italia',
|
||||
Type INTEGER NOT NULL DEFAULT 0,
|
||||
IsDefault INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_Code ON WarehouseLocations(Code);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_IsDefault ON WarehouseLocations(IsDefault);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseLocations_IsActive ON WarehouseLocations(IsActive);
|
||||
|
||||
-- Categorie Articoli
|
||||
CREATE TABLE IF NOT EXISTS WarehouseArticleCategories (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL UNIQUE,
|
||||
Name TEXT NOT NULL,
|
||||
Description TEXT,
|
||||
ParentCategoryId INTEGER,
|
||||
Level INTEGER NOT NULL DEFAULT 0,
|
||||
FullPath TEXT,
|
||||
Icon TEXT,
|
||||
Color TEXT,
|
||||
DefaultValuationMethod INTEGER,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ParentCategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_Code ON WarehouseArticleCategories(Code);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_ParentCategoryId ON WarehouseArticleCategories(ParentCategoryId);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticleCategories_FullPath ON WarehouseArticleCategories(FullPath);
|
||||
|
||||
-- Articoli Magazzino
|
||||
CREATE TABLE IF NOT EXISTS WarehouseArticles (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL UNIQUE,
|
||||
Description TEXT NOT NULL,
|
||||
ShortDescription TEXT,
|
||||
Barcode TEXT,
|
||||
ManufacturerCode TEXT,
|
||||
CategoryId INTEGER,
|
||||
UnitOfMeasure TEXT NOT NULL DEFAULT 'PZ',
|
||||
SecondaryUnitOfMeasure TEXT,
|
||||
UnitConversionFactor REAL,
|
||||
StockManagement INTEGER NOT NULL DEFAULT 0,
|
||||
IsBatchManaged INTEGER NOT NULL DEFAULT 0,
|
||||
IsSerialManaged INTEGER NOT NULL DEFAULT 0,
|
||||
HasExpiry INTEGER NOT NULL DEFAULT 0,
|
||||
ExpiryWarningDays INTEGER,
|
||||
MinimumStock REAL,
|
||||
MaximumStock REAL,
|
||||
ReorderPoint REAL,
|
||||
ReorderQuantity REAL,
|
||||
LeadTimeDays INTEGER,
|
||||
ValuationMethod INTEGER,
|
||||
StandardCost REAL,
|
||||
LastPurchaseCost REAL,
|
||||
WeightedAverageCost REAL,
|
||||
BaseSellingPrice REAL,
|
||||
Weight REAL,
|
||||
Volume REAL,
|
||||
Width REAL,
|
||||
Height REAL,
|
||||
Depth REAL,
|
||||
Image BLOB,
|
||||
ImageMimeType TEXT,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (CategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_Code ON WarehouseArticles(Code);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_Barcode ON WarehouseArticles(Barcode);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_CategoryId ON WarehouseArticles(CategoryId);
|
||||
CREATE INDEX IF NOT EXISTS IX_WarehouseArticles_IsActive ON WarehouseArticles(IsActive);
|
||||
|
||||
-- Partite/Lotti
|
||||
CREATE TABLE IF NOT EXISTS ArticleBatches (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
BatchNumber TEXT NOT NULL,
|
||||
ProductionDate TEXT,
|
||||
ExpiryDate TEXT,
|
||||
SupplierBatch TEXT,
|
||||
SupplierId INTEGER,
|
||||
UnitCost REAL,
|
||||
InitialQuantity REAL NOT NULL DEFAULT 0,
|
||||
CurrentQuantity REAL NOT NULL DEFAULT 0,
|
||||
ReservedQuantity REAL NOT NULL DEFAULT 0,
|
||||
Status INTEGER NOT NULL DEFAULT 0,
|
||||
QualityStatus INTEGER,
|
||||
LastQualityCheckDate TEXT,
|
||||
Certifications TEXT,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE,
|
||||
UNIQUE(ArticleId, BatchNumber)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleBatches_ArticleId_BatchNumber ON ArticleBatches(ArticleId, BatchNumber);
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleBatches_ExpiryDate ON ArticleBatches(ExpiryDate);
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleBatches_Status ON ArticleBatches(Status);
|
||||
|
||||
-- Seriali/Matricole
|
||||
CREATE TABLE IF NOT EXISTS ArticleSerials (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
BatchId INTEGER,
|
||||
SerialNumber TEXT NOT NULL,
|
||||
ManufacturerSerial TEXT,
|
||||
ProductionDate TEXT,
|
||||
WarrantyExpiryDate TEXT,
|
||||
CurrentWarehouseId INTEGER,
|
||||
Status INTEGER NOT NULL DEFAULT 0,
|
||||
UnitCost REAL,
|
||||
SupplierId INTEGER,
|
||||
CustomerId INTEGER,
|
||||
SoldDate TEXT,
|
||||
SalesReference TEXT,
|
||||
Attributes TEXT,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (CurrentWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL,
|
||||
UNIQUE(ArticleId, SerialNumber)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleSerials_ArticleId_SerialNumber ON ArticleSerials(ArticleId, SerialNumber);
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleSerials_Status ON ArticleSerials(Status);
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleSerials_CurrentWarehouseId ON ArticleSerials(CurrentWarehouseId);
|
||||
|
||||
-- Barcode aggiuntivi
|
||||
CREATE TABLE IF NOT EXISTS ArticleBarcodes (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
Barcode TEXT NOT NULL UNIQUE,
|
||||
Type INTEGER NOT NULL DEFAULT 0,
|
||||
Description TEXT,
|
||||
Quantity REAL NOT NULL DEFAULT 1,
|
||||
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleBarcodes_Barcode ON ArticleBarcodes(Barcode);
|
||||
CREATE INDEX IF NOT EXISTS IX_ArticleBarcodes_ArticleId ON ArticleBarcodes(ArticleId);
|
||||
|
||||
-- Giacenze
|
||||
CREATE TABLE IF NOT EXISTS StockLevels (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
WarehouseId INTEGER NOT NULL,
|
||||
BatchId INTEGER,
|
||||
Quantity REAL NOT NULL DEFAULT 0,
|
||||
ReservedQuantity REAL NOT NULL DEFAULT 0,
|
||||
OnOrderQuantity REAL NOT NULL DEFAULT 0,
|
||||
StockValue REAL,
|
||||
UnitCost REAL,
|
||||
LastMovementDate TEXT,
|
||||
LastInventoryDate TEXT,
|
||||
LocationCode TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL,
|
||||
UNIQUE(ArticleId, WarehouseId, BatchId)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_StockLevels_ArticleId_WarehouseId_BatchId ON StockLevels(ArticleId, WarehouseId, BatchId);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockLevels_WarehouseId ON StockLevels(WarehouseId);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockLevels_LocationCode ON StockLevels(LocationCode);
|
||||
|
||||
-- Causali Movimento
|
||||
CREATE TABLE IF NOT EXISTS MovementReasons (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL UNIQUE,
|
||||
Description TEXT NOT NULL,
|
||||
MovementType INTEGER NOT NULL,
|
||||
StockSign INTEGER NOT NULL,
|
||||
RequiresExternalReference INTEGER NOT NULL DEFAULT 0,
|
||||
RequiresValuation INTEGER NOT NULL DEFAULT 1,
|
||||
UpdatesAverageCost INTEGER NOT NULL DEFAULT 1,
|
||||
IsSystem INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_MovementReasons_Code ON MovementReasons(Code);
|
||||
CREATE INDEX IF NOT EXISTS IX_MovementReasons_MovementType ON MovementReasons(MovementType);
|
||||
CREATE INDEX IF NOT EXISTS IX_MovementReasons_IsActive ON MovementReasons(IsActive);
|
||||
|
||||
-- Movimenti di Magazzino (Testata)
|
||||
CREATE TABLE IF NOT EXISTS StockMovements (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
DocumentNumber TEXT NOT NULL UNIQUE,
|
||||
MovementDate TEXT NOT NULL,
|
||||
Type INTEGER NOT NULL,
|
||||
ReasonId INTEGER,
|
||||
SourceWarehouseId INTEGER,
|
||||
DestinationWarehouseId INTEGER,
|
||||
ExternalReference TEXT,
|
||||
ExternalDocumentType INTEGER,
|
||||
SupplierId INTEGER,
|
||||
CustomerId INTEGER,
|
||||
Status INTEGER NOT NULL DEFAULT 0,
|
||||
ConfirmedDate TEXT,
|
||||
ConfirmedBy TEXT,
|
||||
TotalValue REAL,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ReasonId) REFERENCES MovementReasons(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (SourceWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (DestinationWarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovements_DocumentNumber ON StockMovements(DocumentNumber);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovements_MovementDate ON StockMovements(MovementDate);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovements_Type ON StockMovements(Type);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovements_Status ON StockMovements(Status);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovements_ExternalReference ON StockMovements(ExternalReference);
|
||||
|
||||
-- Movimenti di Magazzino (Righe)
|
||||
CREATE TABLE IF NOT EXISTS StockMovementLines (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
MovementId INTEGER NOT NULL,
|
||||
LineNumber INTEGER NOT NULL,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
BatchId INTEGER,
|
||||
SerialId INTEGER,
|
||||
Quantity REAL NOT NULL,
|
||||
UnitOfMeasure TEXT NOT NULL DEFAULT 'PZ',
|
||||
UnitCost REAL,
|
||||
LineValue REAL,
|
||||
SourceLocationCode TEXT,
|
||||
DestinationLocationCode TEXT,
|
||||
ExternalLineReference TEXT,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (MovementId) REFERENCES StockMovements(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (SerialId) REFERENCES ArticleSerials(Id) ON DELETE SET NULL,
|
||||
UNIQUE(MovementId, LineNumber)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovementLines_MovementId_LineNumber ON StockMovementLines(MovementId, LineNumber);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovementLines_ArticleId ON StockMovementLines(ArticleId);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovementLines_BatchId ON StockMovementLines(BatchId);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockMovementLines_SerialId ON StockMovementLines(SerialId);
|
||||
|
||||
-- Valorizzazione Magazzino per Periodo
|
||||
CREATE TABLE IF NOT EXISTS StockValuations (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ValuationDate TEXT NOT NULL,
|
||||
Period INTEGER NOT NULL,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
WarehouseId INTEGER,
|
||||
Quantity REAL NOT NULL DEFAULT 0,
|
||||
Method INTEGER NOT NULL DEFAULT 0,
|
||||
UnitCost REAL NOT NULL DEFAULT 0,
|
||||
TotalValue REAL NOT NULL DEFAULT 0,
|
||||
InboundQuantity REAL NOT NULL DEFAULT 0,
|
||||
InboundValue REAL NOT NULL DEFAULT 0,
|
||||
OutboundQuantity REAL NOT NULL DEFAULT 0,
|
||||
OutboundValue REAL NOT NULL DEFAULT 0,
|
||||
IsClosed INTEGER NOT NULL DEFAULT 0,
|
||||
ClosedDate TEXT,
|
||||
ClosedBy TEXT,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL,
|
||||
UNIQUE(Period, ArticleId, WarehouseId)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_StockValuations_Period_ArticleId_WarehouseId ON StockValuations(Period, ArticleId, WarehouseId);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockValuations_ValuationDate ON StockValuations(ValuationDate);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockValuations_IsClosed ON StockValuations(IsClosed);
|
||||
|
||||
-- Layer Valorizzazione FIFO/LIFO
|
||||
CREATE TABLE IF NOT EXISTS StockValuationLayers (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
WarehouseId INTEGER NOT NULL,
|
||||
BatchId INTEGER,
|
||||
LayerDate TEXT NOT NULL,
|
||||
SourceMovementId INTEGER,
|
||||
OriginalQuantity REAL NOT NULL DEFAULT 0,
|
||||
RemainingQuantity REAL NOT NULL DEFAULT 0,
|
||||
UnitCost REAL NOT NULL DEFAULT 0,
|
||||
IsExhausted INTEGER NOT NULL DEFAULT 0,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (SourceMovementId) REFERENCES StockMovements(Id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_StockValuationLayers_ArticleId_WarehouseId_LayerDate ON StockValuationLayers(ArticleId, WarehouseId, LayerDate);
|
||||
CREATE INDEX IF NOT EXISTS IX_StockValuationLayers_IsExhausted ON StockValuationLayers(IsExhausted);
|
||||
|
||||
-- Inventari Fisici (Testata)
|
||||
CREATE TABLE IF NOT EXISTS InventoryCounts (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL UNIQUE,
|
||||
Description TEXT NOT NULL,
|
||||
InventoryDate TEXT NOT NULL,
|
||||
WarehouseId INTEGER,
|
||||
CategoryId INTEGER,
|
||||
Type INTEGER NOT NULL DEFAULT 0,
|
||||
Status INTEGER NOT NULL DEFAULT 0,
|
||||
StartDate TEXT,
|
||||
EndDate TEXT,
|
||||
ConfirmedDate TEXT,
|
||||
ConfirmedBy TEXT,
|
||||
AdjustmentMovementId INTEGER,
|
||||
PositiveDifferenceValue REAL,
|
||||
NegativeDifferenceValue REAL,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (CategoryId) REFERENCES WarehouseArticleCategories(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (AdjustmentMovementId) REFERENCES StockMovements(Id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_InventoryCounts_Code ON InventoryCounts(Code);
|
||||
CREATE INDEX IF NOT EXISTS IX_InventoryCounts_InventoryDate ON InventoryCounts(InventoryDate);
|
||||
CREATE INDEX IF NOT EXISTS IX_InventoryCounts_Status ON InventoryCounts(Status);
|
||||
|
||||
-- Inventari Fisici (Righe)
|
||||
CREATE TABLE IF NOT EXISTS InventoryCountLines (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
InventoryCountId INTEGER NOT NULL,
|
||||
ArticleId INTEGER NOT NULL,
|
||||
WarehouseId INTEGER NOT NULL,
|
||||
BatchId INTEGER,
|
||||
LocationCode TEXT,
|
||||
TheoreticalQuantity REAL NOT NULL DEFAULT 0,
|
||||
CountedQuantity REAL,
|
||||
UnitCost REAL,
|
||||
CountedAt TEXT,
|
||||
CountedBy TEXT,
|
||||
SecondCountQuantity REAL,
|
||||
SecondCountBy TEXT,
|
||||
Notes TEXT,
|
||||
CreatedAt TEXT,
|
||||
CreatedBy TEXT,
|
||||
UpdatedAt TEXT,
|
||||
UpdatedBy TEXT,
|
||||
FOREIGN KEY (InventoryCountId) REFERENCES InventoryCounts(Id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ArticleId) REFERENCES WarehouseArticles(Id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (WarehouseId) REFERENCES WarehouseLocations(Id) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (BatchId) REFERENCES ArticleBatches(Id) ON DELETE SET NULL,
|
||||
UNIQUE(InventoryCountId, ArticleId, WarehouseId, BatchId)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IX_InventoryCountLines_InventoryCountId_ArticleId ON InventoryCountLines(InventoryCountId, ArticleId, WarehouseId, BatchId);
|
||||
CREATE INDEX IF NOT EXISTS IX_InventoryCountLines_ArticleId ON InventoryCountLines(ArticleId);
|
||||
4354
src/backend/Zentral.Infrastructure/Migrations/20251202233615_SyncModelChanges.Designer.cs
generated
Normal file
4354
src/backend/Zentral.Infrastructure/Migrations/20251202233615_SyncModelChanges.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Zentral.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SyncModelChanges : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ParentProductionOrderId",
|
||||
table: "ProductionOrders",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProductionOrders_ParentProductionOrderId",
|
||||
table: "ProductionOrders",
|
||||
column: "ParentProductionOrderId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ProductionOrders_ProductionOrders_ParentProductionOrderId",
|
||||
table: "ProductionOrders",
|
||||
column: "ParentProductionOrderId",
|
||||
principalTable: "ProductionOrders",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ProductionOrders_ProductionOrders_ParentProductionOrderId",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProductionOrders_ParentProductionOrderId",
|
||||
table: "ProductionOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ParentProductionOrderId",
|
||||
table: "ProductionOrders");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Zentral.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Zentral.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -1335,6 +1335,9 @@ namespace Zentral.Infrastructure.Migrations
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentProductionOrderId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<decimal>("Quantity")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("TEXT");
|
||||
@@ -1358,6 +1361,8 @@ namespace Zentral.Infrastructure.Migrations
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ParentProductionOrderId");
|
||||
|
||||
b.HasIndex("StartDate");
|
||||
|
||||
b.HasIndex("Status");
|
||||
@@ -3782,7 +3787,13 @@ namespace Zentral.Infrastructure.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Zentral.Domain.Entities.Production.ProductionOrder", "ParentProductionOrder")
|
||||
.WithMany("ChildProductionOrders")
|
||||
.HasForeignKey("ParentProductionOrderId");
|
||||
|
||||
b.Navigation("Article");
|
||||
|
||||
b.Navigation("ParentProductionOrder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.Production.ProductionOrderComponent", b =>
|
||||
@@ -4225,6 +4236,8 @@ namespace Zentral.Infrastructure.Migrations
|
||||
|
||||
modelBuilder.Entity("Zentral.Domain.Entities.Production.ProductionOrder", b =>
|
||||
{
|
||||
b.Navigation("ChildProductionOrders");
|
||||
|
||||
b.Navigation("Components");
|
||||
|
||||
b.Navigation("Phases");
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ModuleGuard } from "./components/ModuleGuard";
|
||||
import { useRealTimeUpdates } from "./hooks/useRealTimeUpdates";
|
||||
import { CollaborationProvider } from "./contexts/CollaborationContext";
|
||||
import { ModuleProvider } from "./contexts/ModuleContext";
|
||||
import { TabProvider } from "./contexts/TabContext";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -57,80 +58,82 @@ function App() {
|
||||
<ModuleProvider>
|
||||
<CollaborationProvider>
|
||||
<RealTimeProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="calendario" element={<CalendarioPage />} />
|
||||
<Route path="eventi" element={<EventiPage />} />
|
||||
<Route path="eventi/:id" element={<EventoDetailPage />} />
|
||||
<Route path="clienti" element={<ClientiPage />} />
|
||||
<Route path="location" element={<LocationPage />} />
|
||||
<Route path="articoli" element={<ArticoliPage />} />
|
||||
<Route path="risorse" element={<RisorsePage />} />
|
||||
<Route
|
||||
path="report-templates"
|
||||
element={<ReportTemplatesPage />}
|
||||
/>
|
||||
<Route
|
||||
path="report-editor"
|
||||
element={<ReportEditorPage />}
|
||||
/>
|
||||
<Route
|
||||
path="report-editor/:id"
|
||||
element={<ReportEditorPage />}
|
||||
/>
|
||||
{/* Admin */}
|
||||
<Route path="modules" element={<ModulesAdminPage />} />
|
||||
<Route
|
||||
path="modules/purchase/:code"
|
||||
element={<ModulePurchasePage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/auto-codes"
|
||||
element={<AutoCodesAdminPage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/custom-fields"
|
||||
element={<CustomFieldsAdminPage />}
|
||||
/>
|
||||
{/* Warehouse Module */}
|
||||
<Route
|
||||
path="warehouse/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="warehouse">
|
||||
<WarehouseRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Purchases Module */}
|
||||
<Route
|
||||
path="purchases/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="purchases">
|
||||
<PurchasesRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Sales Module */}
|
||||
<Route
|
||||
path="sales/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="sales">
|
||||
<SalesRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Production Module */}
|
||||
<Route
|
||||
path="production/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="production">
|
||||
<ProductionRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
<TabProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="calendario" element={<CalendarioPage />} />
|
||||
<Route path="eventi" element={<EventiPage />} />
|
||||
<Route path="eventi/:id" element={<EventoDetailPage />} />
|
||||
<Route path="clienti" element={<ClientiPage />} />
|
||||
<Route path="location" element={<LocationPage />} />
|
||||
<Route path="articoli" element={<ArticoliPage />} />
|
||||
<Route path="risorse" element={<RisorsePage />} />
|
||||
<Route
|
||||
path="report-templates"
|
||||
element={<ReportTemplatesPage />}
|
||||
/>
|
||||
<Route
|
||||
path="report-editor"
|
||||
element={<ReportEditorPage />}
|
||||
/>
|
||||
<Route
|
||||
path="report-editor/:id"
|
||||
element={<ReportEditorPage />}
|
||||
/>
|
||||
{/* Admin */}
|
||||
<Route path="modules" element={<ModulesAdminPage />} />
|
||||
<Route
|
||||
path="modules/purchase/:code"
|
||||
element={<ModulePurchasePage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/auto-codes"
|
||||
element={<AutoCodesAdminPage />}
|
||||
/>
|
||||
<Route
|
||||
path="admin/custom-fields"
|
||||
element={<CustomFieldsAdminPage />}
|
||||
/>
|
||||
{/* Warehouse Module */}
|
||||
<Route
|
||||
path="warehouse/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="warehouse">
|
||||
<WarehouseRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Purchases Module */}
|
||||
<Route
|
||||
path="purchases/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="purchases">
|
||||
<PurchasesRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Sales Module */}
|
||||
<Route
|
||||
path="sales/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="sales">
|
||||
<SalesRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
{/* Production Module */}
|
||||
<Route
|
||||
path="production/*"
|
||||
element={
|
||||
<ModuleGuard moduleCode="production">
|
||||
<ProductionRoutes />
|
||||
</ModuleGuard>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</TabProvider>
|
||||
</RealTimeProvider>
|
||||
</CollaborationProvider>
|
||||
</ModuleProvider>
|
||||
|
||||
@@ -1,185 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { Outlet, useNavigate, useLocation } from "react-router-dom";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
List,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Event as EventIcon,
|
||||
People as PeopleIcon,
|
||||
Place as PlaceIcon,
|
||||
Inventory as InventoryIcon,
|
||||
Person as PersonIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
Print as PrintIcon,
|
||||
Close as CloseIcon,
|
||||
Extension as ModulesIcon,
|
||||
Warehouse as WarehouseIcon,
|
||||
Code as AutoCodeIcon,
|
||||
ShoppingCart as ShoppingCartIcon,
|
||||
Sell as SellIcon,
|
||||
Factory as ProductionIcon,
|
||||
} from "@mui/icons-material";
|
||||
import CollaborationIndicator from "./collaboration/CollaborationIndicator";
|
||||
import { useModules } from "../contexts/ModuleContext";
|
||||
import { useLanguage } from "../contexts/LanguageContext";
|
||||
import { SettingsSelector } from "./SettingsSelector";
|
||||
import Sidebar from "./Sidebar";
|
||||
import SearchBar from "./SearchBar";
|
||||
import TabsBar from "./TabsBar";
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
const DRAWER_WIDTH_COLLAPSED = 64;
|
||||
const DRAWER_WIDTH = 280; // Increased width for better readability
|
||||
|
||||
export default function Layout() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const { activeModules } = useModules();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md")); // 600-900px
|
||||
|
||||
// Drawer width based on screen size
|
||||
const drawerWidth = isTablet ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH;
|
||||
|
||||
const menuItems = [
|
||||
{ text: t('menu.dashboard'), icon: <DashboardIcon />, path: "/" },
|
||||
{ text: t('menu.calendar'), icon: <CalendarIcon />, path: "/calendario" },
|
||||
{ text: t('menu.events'), icon: <EventIcon />, path: "/eventi" },
|
||||
{ text: t('menu.clients'), icon: <PeopleIcon />, path: "/clienti" },
|
||||
{ text: t('menu.location'), icon: <PlaceIcon />, path: "/location" },
|
||||
{ text: t('menu.articles'), icon: <InventoryIcon />, path: "/articoli" },
|
||||
{ text: t('menu.resources'), icon: <PersonIcon />, path: "/risorse" },
|
||||
{
|
||||
text: t('menu.warehouse'),
|
||||
icon: <WarehouseIcon />,
|
||||
path: "/warehouse",
|
||||
moduleCode: "warehouse",
|
||||
},
|
||||
{
|
||||
text: t('menu.purchases'),
|
||||
icon: <ShoppingCartIcon />,
|
||||
path: "/purchases/orders",
|
||||
moduleCode: "purchases",
|
||||
},
|
||||
{
|
||||
text: t('menu.sales'),
|
||||
icon: <SellIcon />,
|
||||
path: "/sales/orders",
|
||||
moduleCode: "sales",
|
||||
},
|
||||
{
|
||||
text: t('menu.production'),
|
||||
icon: <ProductionIcon />,
|
||||
path: "/production/orders",
|
||||
moduleCode: "production",
|
||||
},
|
||||
{ text: t('menu.reports'), icon: <PrintIcon />, path: "/report-templates" },
|
||||
{ text: t('menu.modules'), icon: <ModulesIcon />, path: "/modules" },
|
||||
{ text: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: "/admin/auto-codes" },
|
||||
{ text: t('menu.customFields'), icon: <AutoCodeIcon />, path: "/admin/custom-fields" },
|
||||
];
|
||||
|
||||
// Filter menu items based on active modules
|
||||
const activeModuleCodes = activeModules.map((m) => m.code);
|
||||
const filteredMenuItems = menuItems.filter(
|
||||
(item) => !item.moduleCode || activeModuleCodes.includes(item.moduleCode),
|
||||
);
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const drawer = (
|
||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
justifyContent: isTablet ? "center" : "space-between",
|
||||
minHeight: { xs: 56, sm: 64 },
|
||||
}}
|
||||
>
|
||||
{!isTablet && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="div"
|
||||
sx={{ fontWeight: "bold" }}
|
||||
>
|
||||
Zentral
|
||||
</Typography>
|
||||
)}
|
||||
{isMobile && (
|
||||
<IconButton onClick={handleDrawerToggle} sx={{ ml: "auto" }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List sx={{ flex: 1, py: 1 }}>
|
||||
{filteredMenuItems.map((item) => (
|
||||
<ListItem key={item.text} disablePadding sx={{ px: 1 }}>
|
||||
<ListItemButton
|
||||
selected={location.pathname === item.path}
|
||||
onClick={() => {
|
||||
navigate(item.path);
|
||||
if (isMobile) setMobileOpen(false);
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
minHeight: 48,
|
||||
justifyContent: isTablet ? "center" : "flex-start",
|
||||
px: isTablet ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: isTablet ? 0 : 40,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
{!isTablet && <ListItemText primary={item.text} />}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
{!isTablet && (
|
||||
<Box sx={{ p: 2, borderTop: 1, borderColor: "divider" }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
© 2025 Zentral
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", minHeight: "100vh" }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
width: {
|
||||
xs: "100%",
|
||||
sm: `calc(100% - ${drawerWidth}px)`,
|
||||
},
|
||||
ml: { sm: `${drawerWidth}px` },
|
||||
width: { sm: `calc(100% - ${DRAWER_WIDTH}px)` },
|
||||
ml: { sm: `${DRAWER_WIDTH}px` },
|
||||
boxShadow: 1,
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
}}
|
||||
>
|
||||
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 } }}>
|
||||
@@ -192,17 +53,9 @@ export default function Layout() {
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: { xs: "1rem", sm: "1.25rem" },
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{isMobile ? "Zentral" : "Catering & Banqueting Management"}
|
||||
</Typography>
|
||||
|
||||
{/* Search Bar */}
|
||||
<SearchBar />
|
||||
|
||||
{/* Collaboration Indicator */}
|
||||
<CollaborationIndicator compact={isMobile} />
|
||||
@@ -216,7 +69,7 @@ export default function Layout() {
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{
|
||||
width: { sm: drawerWidth },
|
||||
width: { sm: DRAWER_WIDTH },
|
||||
flexShrink: { sm: 0 },
|
||||
}}
|
||||
>
|
||||
@@ -233,26 +86,22 @@ export default function Layout() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
<Sidebar onClose={handleDrawerToggle} />
|
||||
</Drawer>
|
||||
|
||||
{/* Desktop/Tablet Drawer */}
|
||||
{/* Desktop Drawer */}
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: "none", sm: "block" },
|
||||
"& .MuiDrawer-paper": {
|
||||
boxSizing: "border-box",
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
width: DRAWER_WIDTH,
|
||||
},
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawer}
|
||||
<Sidebar />
|
||||
</Drawer>
|
||||
</Box>
|
||||
|
||||
@@ -263,7 +112,7 @@ export default function Layout() {
|
||||
flexGrow: 1,
|
||||
width: {
|
||||
xs: "100vw",
|
||||
sm: `calc(100vw - ${drawerWidth}px)`,
|
||||
sm: `calc(100vw - ${DRAWER_WIDTH}px)`,
|
||||
},
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
@@ -274,19 +123,23 @@ export default function Layout() {
|
||||
{/* Toolbar spacer */}
|
||||
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 }, flexShrink: 0 }} />
|
||||
|
||||
{/* Tabs Bar */}
|
||||
<TabsBar />
|
||||
|
||||
{/* Content */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0, // Important: allows flex children to shrink below content size
|
||||
minHeight: 0,
|
||||
p: location.pathname.startsWith("/report-editor")
|
||||
? 0
|
||||
: { xs: 1.5, sm: 2, md: 3 },
|
||||
overflow: location.pathname.startsWith("/report-editor")
|
||||
? "hidden"
|
||||
: "auto",
|
||||
bgcolor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
164
src/frontend/src/components/SearchBar.tsx
Normal file
164
src/frontend/src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useMemo } from 'react';
|
||||
import { styled, alpha } from '@mui/material/styles';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Autocomplete, Box, Typography } from '@mui/material';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useModules } from '../contexts/ModuleContext';
|
||||
import { useTabs } from '../contexts/TabContext';
|
||||
|
||||
const Search = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||
},
|
||||
marginRight: theme.spacing(2),
|
||||
marginLeft: 0,
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
marginLeft: theme.spacing(3),
|
||||
width: 'auto',
|
||||
minWidth: '300px',
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchIconWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||
color: 'inherit',
|
||||
width: '100%',
|
||||
'& .MuiInputBase-input': {
|
||||
padding: theme.spacing(1, 1, 1, 0),
|
||||
// vertical padding + font size from searchIcon
|
||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface SearchOption {
|
||||
label: string;
|
||||
path: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export default function SearchBar() {
|
||||
const { t } = useLanguage();
|
||||
const { activeModules } = useModules();
|
||||
const { openTab } = useTabs();
|
||||
|
||||
const activeModuleCodes = activeModules.map((m) => m.code);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const opts: SearchOption[] = [
|
||||
// Core
|
||||
{ label: t('menu.dashboard'), path: '/', category: 'Zentral' },
|
||||
{ label: t('menu.calendar'), path: '/calendario', category: 'Zentral' },
|
||||
{ label: t('menu.events'), path: '/eventi', category: 'Zentral' },
|
||||
{ label: t('menu.clients'), path: '/clienti', category: 'Zentral' },
|
||||
{ label: t('menu.location'), path: '/location', category: 'Zentral' },
|
||||
{ label: t('menu.articles'), path: '/articoli', category: 'Zentral' },
|
||||
{ label: t('menu.resources'), path: '/risorse', category: 'Zentral' },
|
||||
{ label: t('menu.reports'), path: '/report-templates', category: 'Zentral' },
|
||||
];
|
||||
|
||||
if (activeModuleCodes.includes('warehouse')) {
|
||||
opts.push(
|
||||
{ label: 'Warehouse Dashboard', path: '/warehouse', category: 'Warehouse' },
|
||||
{ label: 'Warehouse Articles', path: '/warehouse/articles', category: 'Warehouse' },
|
||||
{ label: 'Warehouse Locations', path: '/warehouse/locations', category: 'Warehouse' },
|
||||
{ label: 'Warehouse Movements', path: '/warehouse/movements', category: 'Warehouse' },
|
||||
{ label: 'Warehouse Stock', path: '/warehouse/stock', category: 'Warehouse' },
|
||||
{ label: 'Warehouse Inventory', path: '/warehouse/inventory', category: 'Warehouse' }
|
||||
);
|
||||
}
|
||||
|
||||
if (activeModuleCodes.includes('purchases')) {
|
||||
opts.push(
|
||||
{ label: 'Purchases Suppliers', path: '/purchases/suppliers', category: 'Purchases' },
|
||||
{ label: 'Purchases Orders', path: '/purchases/orders', category: 'Purchases' }
|
||||
);
|
||||
}
|
||||
|
||||
if (activeModuleCodes.includes('sales')) {
|
||||
opts.push(
|
||||
{ label: 'Sales Orders', path: '/sales/orders', category: 'Sales' }
|
||||
);
|
||||
}
|
||||
|
||||
if (activeModuleCodes.includes('production')) {
|
||||
opts.push(
|
||||
{ label: 'Production Dashboard', path: '/production', category: 'Production' },
|
||||
{ label: 'Production Orders', path: '/production/orders', category: 'Production' },
|
||||
{ label: 'Bill of Materials', path: '/production/bom', category: 'Production' },
|
||||
{ label: 'Work Centers', path: '/production/work-centers', category: 'Production' },
|
||||
{ label: 'Production Cycles', path: '/production/cycles', category: 'Production' },
|
||||
{ label: 'MRP', path: '/production/mrp', category: 'Production' }
|
||||
);
|
||||
}
|
||||
|
||||
opts.push(
|
||||
{ label: t('menu.modules'), path: '/modules', category: 'Admin' },
|
||||
{ label: t('menu.autoCodes'), path: '/admin/auto-codes', category: 'Admin' },
|
||||
{ label: t('menu.customFields'), path: '/admin/custom-fields', category: 'Admin' }
|
||||
);
|
||||
|
||||
return opts;
|
||||
}, [activeModuleCodes, t]);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
id="search-bar"
|
||||
disableClearable
|
||||
sx={{ flexGrow: 1, maxWidth: 600, mx: 2 }}
|
||||
options={options}
|
||||
groupBy={(option) => option.category}
|
||||
getOptionLabel={(option) => typeof option === 'string' ? option : option.label}
|
||||
onChange={(_, value) => {
|
||||
if (typeof value !== 'string' && value) {
|
||||
openTab(value.path, value.label);
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
const { InputLabelProps, InputProps, ...rest } = params;
|
||||
return (
|
||||
<Search>
|
||||
<SearchIconWrapper>
|
||||
<SearchIcon />
|
||||
</SearchIconWrapper>
|
||||
<StyledInputBase
|
||||
{...InputProps}
|
||||
{...rest}
|
||||
placeholder="Search..."
|
||||
inputProps={{ ...params.inputProps, 'aria-label': 'search' }}
|
||||
/>
|
||||
</Search>
|
||||
);
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
return (
|
||||
<li {...props} key={option.path}>
|
||||
<Box>
|
||||
<Typography variant="body2">{option.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{option.category}
|
||||
</Typography>
|
||||
</Box>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
225
src/frontend/src/components/Sidebar.tsx
Normal file
225
src/frontend/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
Box,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ExpandLess,
|
||||
ExpandMore,
|
||||
Dashboard as DashboardIcon,
|
||||
Event as EventIcon,
|
||||
People as PeopleIcon,
|
||||
Place as PlaceIcon,
|
||||
Inventory as InventoryIcon,
|
||||
Person as PersonIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
Print as PrintIcon,
|
||||
Extension as ModulesIcon,
|
||||
Warehouse as WarehouseIcon,
|
||||
Code as AutoCodeIcon,
|
||||
ShoppingCart as ShoppingCartIcon,
|
||||
Sell as SellIcon,
|
||||
Factory as ProductionIcon,
|
||||
Settings as SettingsIcon,
|
||||
Storage as StorageIcon,
|
||||
SwapHoriz as SwapIcon,
|
||||
Assignment as AssignmentIcon,
|
||||
ListAlt as ListAltIcon,
|
||||
Build as BuildIcon,
|
||||
Timeline as TimelineIcon,
|
||||
PrecisionManufacturing as ManufacturingIcon,
|
||||
Category as CategoryIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useModules } from '../contexts/ModuleContext';
|
||||
import { useTabs } from '../contexts/TabContext';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
path?: string;
|
||||
children?: MenuItem[];
|
||||
moduleCode?: string;
|
||||
}
|
||||
|
||||
export default function Sidebar({ onClose }: { onClose?: () => void }) {
|
||||
const { t } = useLanguage();
|
||||
const { activeModules } = useModules();
|
||||
const { openTab } = useTabs();
|
||||
const location = useLocation();
|
||||
const [openItems, setOpenItems] = useState<Record<string, boolean>>({
|
||||
core: true,
|
||||
warehouse: false,
|
||||
purchases: false,
|
||||
sales: false,
|
||||
production: false,
|
||||
admin: false,
|
||||
});
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setOpenItems((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const handleItemClick = (item: MenuItem) => {
|
||||
if (item.path) {
|
||||
openTab(item.path, item.label);
|
||||
if (onClose) onClose();
|
||||
} else if (item.children) {
|
||||
handleToggle(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const menuStructure: MenuItem[] = [
|
||||
{
|
||||
id: 'core',
|
||||
label: 'Zentral',
|
||||
icon: <DashboardIcon />,
|
||||
children: [
|
||||
{ id: 'dashboard', label: t('menu.dashboard'), icon: <DashboardIcon />, path: '/' },
|
||||
{ id: 'calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/calendario' },
|
||||
{ id: 'events', label: t('menu.events'), icon: <EventIcon />, path: '/eventi' },
|
||||
{ id: 'clients', label: t('menu.clients'), icon: <PeopleIcon />, path: '/clienti' },
|
||||
{ id: 'location', label: t('menu.location'), icon: <PlaceIcon />, path: '/location' },
|
||||
{ id: 'articles', label: t('menu.articles'), icon: <InventoryIcon />, path: '/articoli' },
|
||||
{ id: 'resources', label: t('menu.resources'), icon: <PersonIcon />, path: '/risorse' },
|
||||
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-templates' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'warehouse',
|
||||
label: t('menu.warehouse'),
|
||||
icon: <WarehouseIcon />,
|
||||
moduleCode: 'warehouse',
|
||||
children: [
|
||||
{ id: 'wh-dashboard', label: 'Dashboard', icon: <DashboardIcon />, path: '/warehouse' },
|
||||
{ id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles' },
|
||||
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations' },
|
||||
{ id: 'wh-movements', label: 'Movimenti', icon: <SwapIcon />, path: '/warehouse/movements' },
|
||||
{ id: 'wh-stock', label: 'Giacenze', icon: <StorageIcon />, path: '/warehouse/stock' },
|
||||
{ id: 'wh-inventory', label: 'Inventario', icon: <AssignmentIcon />, path: '/warehouse/inventory' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'purchases',
|
||||
label: t('menu.purchases'),
|
||||
icon: <ShoppingCartIcon />,
|
||||
moduleCode: 'purchases',
|
||||
children: [
|
||||
{ id: 'pur-suppliers', label: 'Fornitori', icon: <PeopleIcon />, path: '/purchases/suppliers' },
|
||||
{ id: 'pur-orders', label: 'Ordini Acquisto', icon: <ListAltIcon />, path: '/purchases/orders' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sales',
|
||||
label: t('menu.sales'),
|
||||
icon: <SellIcon />,
|
||||
moduleCode: 'sales',
|
||||
children: [
|
||||
{ id: 'sal-orders', label: 'Ordini Vendita', icon: <ListAltIcon />, path: '/sales/orders' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'production',
|
||||
label: t('menu.production'),
|
||||
icon: <ProductionIcon />,
|
||||
moduleCode: 'production',
|
||||
children: [
|
||||
{ id: 'prod-dashboard', label: 'Dashboard', icon: <DashboardIcon />, path: '/production' },
|
||||
{ id: 'prod-orders', label: 'Ordini Produzione', icon: <ListAltIcon />, path: '/production/orders' },
|
||||
{ id: 'prod-bom', label: 'Distinte Base', icon: <AssignmentIcon />, path: '/production/bom' },
|
||||
{ id: 'prod-workcenters', label: 'Centri di Lavoro', icon: <BuildIcon />, path: '/production/work-centers' },
|
||||
{ id: 'prod-cycles', label: 'Cicli', icon: <TimelineIcon />, path: '/production/cycles' },
|
||||
{ id: 'prod-mrp', label: 'MRP', icon: <ManufacturingIcon />, path: '/production/mrp' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
label: 'Amministrazione',
|
||||
icon: <SettingsIcon />,
|
||||
children: [
|
||||
{ id: 'modules', label: t('menu.modules'), icon: <ModulesIcon />, path: '/modules' },
|
||||
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes' },
|
||||
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const activeModuleCodes = activeModules.map((m) => m.code);
|
||||
|
||||
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
||||
// Filter by module
|
||||
if (item.moduleCode && !activeModuleCodes.includes(item.moduleCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isOpen = openItems[item.id];
|
||||
const isSelected = item.path ? location.pathname === item.path : false;
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<ListItem disablePadding sx={{ display: 'block' }}>
|
||||
<ListItemButton
|
||||
onClick={() => handleItemClick(item)}
|
||||
selected={isSelected}
|
||||
sx={{
|
||||
minHeight: 48,
|
||||
justifyContent: 'initial',
|
||||
px: 2.5,
|
||||
pl: level * 2 + 2.5,
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
mr: 3,
|
||||
justifyContent: 'center',
|
||||
color: isSelected ? 'primary.main' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.label}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: isSelected ? 'bold' : 'medium',
|
||||
fontSize: level === 0 ? '0.95rem' : '0.9rem',
|
||||
}}
|
||||
/>
|
||||
{hasChildren ? (isOpen ? <ExpandLess /> : <ExpandMore />) : null}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{hasChildren && (
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{item.children?.map((child) => renderMenuItem(child, level + 1))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
|
||||
Zentral
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
<List>
|
||||
{menuStructure.map((item) => renderMenuItem(item))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
88
src/frontend/src/components/TabsBar.tsx
Normal file
88
src/frontend/src/components/TabsBar.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { Box, Tabs, Tab, IconButton } from '@mui/material';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { useTabs } from '../contexts/TabContext';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export default function TabsBar() {
|
||||
const { tabs, activeTabPath, setActiveTab, closeTab } = useTabs();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = (_: React.SyntheticEvent, newValue: string) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
minHeight: 48,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': {
|
||||
height: 4,
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={activeTabPath}
|
||||
onChange={handleChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
aria-label="app tabs"
|
||||
sx={{
|
||||
minHeight: 48,
|
||||
'& .MuiTab-root': {
|
||||
minHeight: 48,
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
px: 2,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.path}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<span>{tab.label}</span>
|
||||
{tab.closable && (
|
||||
<IconButton
|
||||
size="small"
|
||||
component="span"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeTab(tab.path);
|
||||
}}
|
||||
sx={{
|
||||
p: 0.5,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover',
|
||||
color: 'error.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
value={tab.path}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
117
src/frontend/src/contexts/TabContext.tsx
Normal file
117
src/frontend/src/contexts/TabContext.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface Tab {
|
||||
path: string;
|
||||
label: string;
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
interface TabContextType {
|
||||
tabs: Tab[];
|
||||
activeTabPath: string;
|
||||
openTab: (path: string, label: string, closable?: boolean) => void;
|
||||
closeTab: (path: string) => void;
|
||||
setActiveTab: (path: string) => void;
|
||||
}
|
||||
|
||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||
|
||||
export function TabProvider({ children }: { children: ReactNode }) {
|
||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||
const [activeTabPath, setActiveTabPath] = useState<string>('/');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Load tabs from local storage on mount
|
||||
useEffect(() => {
|
||||
const savedTabs = localStorage.getItem('zentral_tabs');
|
||||
const savedActiveTab = localStorage.getItem('zentral_active_tab');
|
||||
|
||||
if (savedTabs) {
|
||||
try {
|
||||
const parsedTabs = JSON.parse(savedTabs);
|
||||
if (Array.isArray(parsedTabs) && parsedTabs.length > 0) {
|
||||
setTabs(parsedTabs);
|
||||
} else {
|
||||
// Default tab
|
||||
setTabs([{ path: '/', label: 'Dashboard', closable: false }]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse tabs", e);
|
||||
setTabs([{ path: '/', label: 'Dashboard', closable: false }]);
|
||||
}
|
||||
} else {
|
||||
setTabs([{ path: '/', label: 'Dashboard', closable: false }]);
|
||||
}
|
||||
|
||||
if (savedActiveTab) {
|
||||
setActiveTabPath(savedActiveTab);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save tabs to local storage whenever they change
|
||||
useEffect(() => {
|
||||
if (tabs.length > 0) {
|
||||
localStorage.setItem('zentral_tabs', JSON.stringify(tabs));
|
||||
}
|
||||
localStorage.setItem('zentral_active_tab', activeTabPath);
|
||||
}, [tabs, activeTabPath]);
|
||||
|
||||
// Sync active tab with location
|
||||
useEffect(() => {
|
||||
// If the current location is not the active tab, update active tab if it exists in tabs
|
||||
// This handles browser back/forward buttons
|
||||
const currentPath = location.pathname;
|
||||
const tabExists = tabs.find(t => t.path === currentPath);
|
||||
if (tabExists && activeTabPath !== currentPath) {
|
||||
setActiveTabPath(currentPath);
|
||||
}
|
||||
}, [location.pathname, tabs, activeTabPath]);
|
||||
|
||||
const openTab = (path: string, label: string, closable: boolean = true) => {
|
||||
if (!tabs.find((t) => t.path === path)) {
|
||||
setTabs((prev) => [...prev, { path, label, closable }]);
|
||||
}
|
||||
setActiveTabPath(path);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
const closeTab = (path: string) => {
|
||||
const tabIndex = tabs.findIndex((t) => t.path === path);
|
||||
if (tabIndex === -1) return;
|
||||
|
||||
const newTabs = tabs.filter((t) => t.path !== path);
|
||||
setTabs(newTabs);
|
||||
|
||||
if (activeTabPath === path) {
|
||||
// Switch to the nearest tab
|
||||
const newActiveTab = newTabs[tabIndex] || newTabs[tabIndex - 1] || newTabs[0];
|
||||
if (newActiveTab) {
|
||||
setActiveTabPath(newActiveTab.path);
|
||||
navigate(newActiveTab.path);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveTab = (path: string) => {
|
||||
setActiveTabPath(path);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabContext.Provider value={{ tabs, activeTabPath, openTab, closeTab, setActiveTab }}>
|
||||
{children}
|
||||
</TabContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTabs() {
|
||||
const context = useContext(TabContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTabs must be used within a TabProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user