1432 lines
70 KiB
C#
1432 lines
70 KiB
C#
using Lagerbestand.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using MySqlConnector;
|
|
|
|
namespace Lagerbestand.Services;
|
|
|
|
public interface IDatabaseInitializer
|
|
{
|
|
Task InitializeAsync(CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public class DatabaseInitializer : IDatabaseInitializer
|
|
{
|
|
private const int MaxRetries = 15;
|
|
private const int InitialRetryDelaySeconds = 2;
|
|
private const int MaxRetryDelaySeconds = 30;
|
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<DatabaseInitializer> _logger;
|
|
|
|
public DatabaseInitializer(
|
|
IServiceProvider serviceProvider,
|
|
IConfiguration configuration,
|
|
ILogger<DatabaseInitializer> logger)
|
|
{
|
|
_serviceProvider = serviceProvider;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var connectionString = _configuration.GetConnectionString("DefaultConnection");
|
|
_logger.LogInformation("Starte Datenbankinitialisierung für Server: {Server}",
|
|
GetServerFromConnectionString(connectionString));
|
|
|
|
var retryDelay = InitialRetryDelaySeconds;
|
|
|
|
for (var attempt = 1; attempt <= MaxRetries; attempt++)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation("Datenbankverbindung wird hergestellt (Versuch {Attempt}/{MaxRetries})...",
|
|
attempt, MaxRetries);
|
|
|
|
// First ensure the database exists
|
|
await EnsureDatabaseExistsAsync(cancellationToken);
|
|
|
|
using var scope = _serviceProvider.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
|
|
// Test connection with timeout
|
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
cts.CancelAfter(TimeSpan.FromSeconds(30));
|
|
|
|
if (await context.Database.CanConnectAsync(cts.Token))
|
|
{
|
|
_logger.LogInformation("Datenbankverbindung erfolgreich hergestellt.");
|
|
|
|
await CreateTablesAsync(cancellationToken);
|
|
|
|
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
|
|
await authService.EnsureAdminExistsAsync();
|
|
|
|
var storageLocationService = scope.ServiceProvider.GetRequiredService<IStorageLocationService>();
|
|
var seeded = await storageLocationService.SeedDefaultLocationsAsync();
|
|
if (seeded > 0)
|
|
_logger.LogInformation("{Count} Standard-Lagerplätze angelegt.", seeded);
|
|
|
|
_logger.LogInformation("Datenbankinitialisierung abgeschlossen.");
|
|
return;
|
|
}
|
|
|
|
_logger.LogWarning("Datenbankverbindung fehlgeschlagen (CanConnect returned false).");
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
_logger.LogWarning("Datenbankinitialisierung wurde abgebrochen.");
|
|
throw;
|
|
}
|
|
catch (MySqlException ex)
|
|
{
|
|
_logger.LogWarning(ex, "MySQL Fehler bei Datenbankinitialisierung (Versuch {Attempt}/{MaxRetries}): {Message}",
|
|
attempt, MaxRetries, ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Datenbankinitialisierung fehlgeschlagen (Versuch {Attempt}/{MaxRetries}): {Message}",
|
|
attempt, MaxRetries, ex.Message);
|
|
}
|
|
|
|
if (attempt < MaxRetries)
|
|
{
|
|
_logger.LogInformation("Warte {Delay} Sekunden vor dem nächsten Versuch...", retryDelay);
|
|
await Task.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken);
|
|
|
|
// Exponential backoff with max limit
|
|
retryDelay = Math.Min(retryDelay * 2, MaxRetryDelaySeconds);
|
|
}
|
|
}
|
|
|
|
_logger.LogError("Konnte keine Datenbankverbindung herstellen nach {MaxRetries} Versuchen. " +
|
|
"Bitte überprüfen Sie die Verbindungseinstellungen und die Erreichbarkeit des Datenbankservers.", MaxRetries);
|
|
}
|
|
|
|
private static string? GetServerFromConnectionString(string? connectionString)
|
|
{
|
|
if (string.IsNullOrEmpty(connectionString)) return null;
|
|
|
|
foreach (var part in connectionString.Split(';'))
|
|
{
|
|
var kv = part.Split('=', 2);
|
|
if (kv.Length == 2 && kv[0].Trim().Equals("Server", StringComparison.OrdinalIgnoreCase))
|
|
return kv[1].Trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async Task CreateTablesAsync(CancellationToken cancellationToken)
|
|
{
|
|
var connectionString = _configuration.GetConnectionString("DefaultConnection")
|
|
?? throw new InvalidOperationException("Connection string nicht gefunden.");
|
|
|
|
await using var connection = new MySqlConnection(connectionString);
|
|
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
// Core tables
|
|
await ExecuteSqlSafeAsync(connection, CreateCatalogItemsTableSql, "CatalogItems", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateInventoryItemsTableSql, "InventoryItems", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateUsersTableSql, "Users", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateSiteSettingsTableSql, "SiteSettings", cancellationToken);
|
|
|
|
// Tire management tables
|
|
await ExecuteSqlSafeAsync(connection, CreateCustomersTableSql, "Customers", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateVehiclesTableSql, "Vehicles", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateStorageLocationsTableSql, "StorageLocations", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateTireStacksTableSql, "TireStacks", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateTireItemsTableSql, "TireItems", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateTireInspectionsTableSql, "TireInspections", cancellationToken);
|
|
|
|
// Kanban tables
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanColumnsTableSql, "KanbanColumns", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanLabelsTableSql, "KanbanLabels", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTasksTableSql, "KanbanTasks", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskTodosTableSql, "KanbanTaskTodos", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskCommentsTableSql, "KanbanTaskComments", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskNotesTableSql, "KanbanTaskNotes", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskAttachmentsTableSql, "KanbanTaskAttachments", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskTemplatesTableSql, "KanbanTaskTemplates", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskTemplateTodosTableSql, "KanbanTaskTemplateTodos", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskLabelsTableSql, "KanbanTaskLabels", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskTemplateLabelsTableSql, "KanbanTaskTemplateLabels", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskAssigneesTableSql, "KanbanTaskAssignees", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskCustomersTableSql, "KanbanTaskCustomers", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, CreateKanbanTaskTireStacksTableSql, "KanbanTaskTireStacks", cancellationToken);
|
|
|
|
// Initialize default Kanban columns
|
|
await InitializeDefaultKanbanColumnsAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add VehicleId column to TireStacks if it doesn't exist
|
|
await MigrateAddVehicleIdToTireStacksAsync(connection, cancellationToken);
|
|
|
|
// Migration: Migrate customer vehicle data to Vehicles table
|
|
await MigrateCustomerVehiclesToVehiclesTableAsync(connection, cancellationToken);
|
|
|
|
// Migration: Migrate old TireSets table to TireStacks if it exists
|
|
await MigrateTireSetsToTireStacksAsync(connection, cancellationToken);
|
|
|
|
// Migration: Migrate old StorageLocations schema to new ABCD format
|
|
await MigrateStorageLocationsSchemaAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add Position column (Gebäude/Reihe/Seite-Ebene/Platz format) and reset test data
|
|
await MigrateStorageLocationsPositionAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add FilePath column to KanbanTaskAttachments for external file storage
|
|
await MigrateAddFilePathToKanbanTaskAttachmentsAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add NotesText column to KanbanTasks for simple task notes
|
|
await MigrateAddNotesTextToKanbanTasksAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add IsActive column to TireStacks for soft delete
|
|
await MigrateAddIsActiveToTireStacksAsync(connection, cancellationToken);
|
|
|
|
// Migration: Remove LicensePlate/Year from Vehicles, add TireSize, make Make NOT NULL
|
|
await MigrateVehiclesRemoveLicensePlateAsync(connection, cancellationToken);
|
|
|
|
// Migration: Rename Kanban column "Warte auf wen anders" to "Warte auf Kunden"
|
|
await MigrateRenameKanbanWaitColumnAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add TireSizeNote column to KanbanTasks for quick tire size notes
|
|
await MigrateAddTireSizeNoteToKanbanTasksAsync(connection, cancellationToken);
|
|
|
|
// Migration: Fix broken Kanban column icons (corrupted emoji in seed data)
|
|
await MigrateFixKanbanColumnIconsAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add VehicleId column to KanbanTaskCustomers for vehicle selection
|
|
await MigrateAddVehicleIdToKanbanTaskCustomersAsync(connection, cancellationToken);
|
|
|
|
// Migration: Make Customer FirstName nullable
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Customers` MODIFY COLUMN `FirstName` VARCHAR(100) NULL;",
|
|
"MakeCustomerFirstNameNullable", cancellationToken);
|
|
|
|
// Data repair: backfill NULLs in columns mapped as non-nullable strings
|
|
// (prevents InvalidCastException: DBNull -> String when materializing entities)
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `Customers` SET `LastName` = '-' WHERE `LastName` IS NULL;",
|
|
"BackfillCustomerLastName", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `Customers` SET `CustomerNumber` = CONCAT('AUTO-', SUBSTRING(REPLACE(`Id`, '-', ''), 1, 12)) WHERE `CustomerNumber` IS NULL;",
|
|
"BackfillCustomerNumber", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Customers` MODIFY COLUMN `LastName` VARCHAR(100) NOT NULL, MODIFY COLUMN `CustomerNumber` VARCHAR(20) NOT NULL;",
|
|
"EnforceCustomerNotNullColumns", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `Vehicles` SET `Make` = 'Unbekannt' WHERE `Make` IS NULL;",
|
|
"BackfillVehicleMake", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `Vehicles` SET `TireSize` = '' WHERE `TireSize` IS NULL;",
|
|
"BackfillVehicleTireSize", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Vehicles` MODIFY COLUMN `Make` VARCHAR(100) NOT NULL, MODIFY COLUMN `TireSize` VARCHAR(50) NOT NULL DEFAULT '';",
|
|
"EnforceVehicleNotNullColumns", cancellationToken);
|
|
|
|
// Migration: Add 2FA columns to Users table
|
|
await MigrateAdd2FaColumnsToUsersAsync(connection, cancellationToken);
|
|
|
|
// Cleanup: Drop legacy Customer columns (LicensePlate, VehicleMake, VehicleModel)
|
|
await DropColumnIfExistsAsync(connection, "Customers", "LicensePlate", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "Customers", "VehicleMake", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "Customers", "VehicleModel", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, "ALTER TABLE `Customers` DROP INDEX `IX_Customers_LicensePlate`;", "DropCustomerLicensePlateIndex", cancellationToken);
|
|
|
|
// Cleanup old schema elements
|
|
await DropColumnIfExistsAsync(connection, "InventoryItems", "Quantity", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "InventoryItems", "LastScannedAt", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "InventoryItems", "Status", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "InventoryItems", "RemovedAt", cancellationToken);
|
|
await ExecuteSqlSafeAsync(connection, "ALTER TABLE `InventoryItems` DROP INDEX `IX_InventoryItems_Status`;", "DropOldIndex", cancellationToken);
|
|
|
|
// Migration: Add Type column to CatalogItems for Reifen/Felgen category distinction
|
|
await MigrateAddTypeToCatalogItemsAsync(connection, cancellationToken);
|
|
|
|
// Migration: Add ItemCount column to TireStacks for variable inventory counts
|
|
await MigrateAddItemCountToTireStacksAsync(connection, cancellationToken);
|
|
|
|
_logger.LogInformation("Datenbanktabellen wurden erstellt/überprüft.");
|
|
}
|
|
|
|
private async Task MigrateTireSetsToTireStacksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'TireSets';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var tireSetsExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (tireSetsExists)
|
|
{
|
|
await using var countCmd = new MySqlCommand("SELECT COUNT(*) FROM `TireStacks`;", connection);
|
|
countCmd.CommandTimeout = 30;
|
|
var tireStacksCount = Convert.ToInt32(await countCmd.ExecuteScalarAsync(cancellationToken));
|
|
|
|
if (tireStacksCount == 0)
|
|
{
|
|
_logger.LogInformation("Migriere Daten von TireSets zu TireStacks...");
|
|
|
|
await ExecuteSqlSafeAsync(connection, """
|
|
INSERT INTO `TireStacks`
|
|
(`Id`, `TicketNumber`, `CustomerId`, `StorageLocationId`, `CatalogItemId`,
|
|
`Status`, `TireSize`, `TireBrand`, `TireModel`, `Season`, `TreadDepth`,
|
|
`DotCode`, `WithRims`, `RimSize`, `HasTpms`, `Condition`, `Notes`,
|
|
`StoredAt`, `PickedUpAt`, `ExpectedPickupDate`, `StoredByUserId`,
|
|
`PickedUpByUserId`, `CreatedAt`, `UpdatedAt`)
|
|
SELECT
|
|
`Id`, `TicketNumber`, `CustomerId`, `StorageLocationId`, `CatalogItemId`,
|
|
`Status`, `TireSize`, `TireBrand`, `TireModel`, `Season`, `TreadDepth`,
|
|
`DotCode`, `WithRims`, `RimSize`, `HasTpms`, 1, `Notes`,
|
|
`StoredAt`, `PickedUpAt`, `ExpectedPickupDate`, `StoredByUserId`,
|
|
`PickedUpByUserId`, `CreatedAt`, `UpdatedAt`
|
|
FROM `TireSets`;
|
|
""", "MigrateTireSetsData", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireItems` DROP FOREIGN KEY `FK_TireItems_TireSets`;",
|
|
"DropTireItemsFK", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireItems` CHANGE COLUMN `TireSetId` `TireStackId` CHAR(36) CHARACTER SET ascii NOT NULL;",
|
|
"RenameTireSetIdColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireItems` ADD CONSTRAINT `FK_TireItems_TireStacks` FOREIGN KEY (`TireStackId`) REFERENCES `TireStacks` (`Id`) ON DELETE CASCADE;",
|
|
"AddTireItemsFK", cancellationToken);
|
|
|
|
_logger.LogInformation("Migration von TireSets zu TireStacks abgeschlossen.");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration von TireSets zu TireStacks übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddVehicleIdToTireStacksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Check if VehicleId column exists
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'TireStacks' AND column_name = 'VehicleId';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var vehicleIdExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!vehicleIdExists)
|
|
{
|
|
_logger.LogInformation("Füge VehicleId Spalte zu TireStacks hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD COLUMN `VehicleId` CHAR(36) CHARACTER SET ascii NULL AFTER `CustomerId`;",
|
|
"AddVehicleIdColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD INDEX `IX_TireStacks_VehicleId` (`VehicleId`);",
|
|
"AddVehicleIdIndex", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD CONSTRAINT `FK_TireStacks_Vehicles` FOREIGN KEY (`VehicleId`) REFERENCES `Vehicles` (`Id`) ON DELETE SET NULL;",
|
|
"AddVehicleIdFK", cancellationToken);
|
|
|
|
_logger.LogInformation("VehicleId Spalte zu TireStacks hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für VehicleId übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateCustomerVehiclesToVehiclesTableAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Skip migration entirely if the legacy LicensePlate column no longer exists
|
|
await using var columnCheckCmd = new MySqlCommand("""
|
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = 'Customers'
|
|
AND COLUMN_NAME = 'LicensePlate';
|
|
""", connection);
|
|
columnCheckCmd.CommandTimeout = 30;
|
|
var hasLegacyColumn = Convert.ToInt32(await columnCheckCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!hasLegacyColumn)
|
|
return;
|
|
|
|
// Check if there are customers with LicensePlate but no vehicles
|
|
await using var checkCmd = new MySqlCommand("""
|
|
SELECT COUNT(*) FROM `Customers` c
|
|
WHERE c.LicensePlate IS NOT NULL
|
|
AND c.LicensePlate != ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM `Vehicles` v
|
|
WHERE v.CustomerId = c.Id AND v.LicensePlate = c.LicensePlate
|
|
);
|
|
""", connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var customersToMigrate = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken));
|
|
|
|
if (customersToMigrate > 0)
|
|
{
|
|
_logger.LogInformation("Migriere {Count} Kundenfahrzeuge in Vehicles-Tabelle...", customersToMigrate);
|
|
|
|
// Insert vehicles from customer data
|
|
await ExecuteSqlSafeAsync(connection, """
|
|
INSERT INTO `Vehicles` (`Id`, `CustomerId`, `LicensePlate`, `Make`, `Model`, `IsPrimary`, `IsActive`, `CreatedAt`)
|
|
SELECT
|
|
UUID(),
|
|
c.Id,
|
|
c.LicensePlate,
|
|
c.VehicleMake,
|
|
c.VehicleModel,
|
|
1,
|
|
1,
|
|
COALESCE(c.CreatedAt, UTC_TIMESTAMP())
|
|
FROM `Customers` c
|
|
WHERE c.LicensePlate IS NOT NULL
|
|
AND c.LicensePlate != ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM `Vehicles` v
|
|
WHERE v.CustomerId = c.Id AND v.LicensePlate = c.LicensePlate
|
|
);
|
|
""", "MigrateCustomerVehicles", cancellationToken);
|
|
|
|
// Update TireStacks to reference the new vehicles
|
|
await ExecuteSqlSafeAsync(connection, """
|
|
UPDATE `TireStacks` ts
|
|
INNER JOIN `Vehicles` v ON v.CustomerId = ts.CustomerId AND v.IsPrimary = 1
|
|
SET ts.VehicleId = v.Id
|
|
WHERE ts.CustomerId IS NOT NULL AND ts.VehicleId IS NULL;
|
|
""", "UpdateTireStackVehicleIds", cancellationToken);
|
|
|
|
_logger.LogInformation("Kundenfahrzeuge erfolgreich migriert.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration der Kundenfahrzeuge übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the Position column for the Gebäude/Reihe/Seite-Ebene/Platz code format.
|
|
/// Existing locations use the old code semantics and are only test data,
|
|
/// so they are removed; the default locations are re-seeded afterwards in InitializeAsync.
|
|
/// </summary>
|
|
private async Task MigrateStorageLocationsPositionAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'StorageLocations' AND column_name = 'Position';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var positionColumnExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (positionColumnExists)
|
|
return;
|
|
|
|
_logger.LogInformation("Migriere StorageLocations auf Gebäude/Reihe/Seite-Ebene/Platz-Format...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `Position` INT NOT NULL DEFAULT 1 AFTER `Row`;",
|
|
"AddPositionColumn", cancellationToken);
|
|
|
|
// Old codes/rows are incompatible with the new format (test data only) - remove them
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `TireStacks` SET `StorageLocationId` = NULL WHERE `StorageLocationId` IS NOT NULL;",
|
|
"DetachTireStacksFromLocations", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"DELETE FROM `StorageLocations`;",
|
|
"ClearOldStorageLocations", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD UNIQUE INDEX `IX_StorageLocations_Area_Row_Position_Slot` (`Area`, `Row`, `Position`, `SlotNumber`);",
|
|
"AddPositionIndex", cancellationToken);
|
|
|
|
_logger.LogInformation("StorageLocations Position-Migration abgeschlossen.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "StorageLocations Position-Migration übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateStorageLocationsSchemaAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Check if new columns exist
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'StorageLocations' AND column_name = 'Area';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var areaColumnExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!areaColumnExists)
|
|
{
|
|
_logger.LogInformation("Migriere StorageLocations Schema zu ABCD-Format...");
|
|
|
|
// Add new columns
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `Area` INT NOT NULL DEFAULT 1;",
|
|
"AddAreaColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `SlotNumber` INT NOT NULL DEFAULT 1;",
|
|
"AddSlotNumberColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `IsOccupied` TINYINT(1) NOT NULL DEFAULT 0;",
|
|
"AddIsOccupiedColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `TireStackId` CHAR(36) CHARACTER SET ascii NULL;",
|
|
"AddTireStackIdColumn", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD COLUMN `OccupiedSince` DATETIME(6) NULL;",
|
|
"AddOccupiedSinceColumn", cancellationToken);
|
|
|
|
// Add index and foreign key for TireStackId
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD INDEX `IX_StorageLocations_TireStackId` (`TireStackId`);",
|
|
"AddTireStackIdIndex", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD INDEX `IX_StorageLocations_Area_Row` (`Area`, `Row`);",
|
|
"AddAreaRowIndex", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` ADD INDEX `IX_StorageLocations_IsOccupied` (`IsOccupied`);",
|
|
"AddIsOccupiedIndex", cancellationToken);
|
|
|
|
// Modify Row column to INT
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `StorageLocations` MODIFY COLUMN `Row` INT NOT NULL DEFAULT 1;",
|
|
"ModifyRowColumn", cancellationToken);
|
|
|
|
// Drop old columns that are no longer needed
|
|
await DropColumnIfExistsAsync(connection, "StorageLocations", "Section", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "StorageLocations", "Capacity", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "StorageLocations", "Name", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "StorageLocations", "QrCode", cancellationToken);
|
|
|
|
_logger.LogInformation("StorageLocations Schema-Migration abgeschlossen.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "StorageLocations Schema-Migration übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddFilePathToKanbanTaskAttachmentsAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Check if FilePath column exists
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'KanbanTaskAttachments' AND column_name = 'FilePath';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var filePathExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!filePathExists)
|
|
{
|
|
_logger.LogInformation("Füge FilePath Spalte zu KanbanTaskAttachments hinzu...");
|
|
|
|
// Add FilePath column
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTaskAttachments` ADD COLUMN `FilePath` VARCHAR(500) NULL AFTER `FileSize`;",
|
|
"AddFilePathColumn", cancellationToken);
|
|
|
|
// Make FileContent nullable for external storage support
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTaskAttachments` MODIFY COLUMN `FileContent` LONGBLOB NULL;",
|
|
"MakeFileContentNullable", cancellationToken);
|
|
|
|
_logger.LogInformation("FilePath Spalte zu KanbanTaskAttachments hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für FilePath übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddNotesTextToKanbanTasksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Check if NotesText column exists
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'KanbanTasks' AND column_name = 'NotesText';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var notesTextExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!notesTextExists)
|
|
{
|
|
_logger.LogInformation("Füge NotesText Spalte zu KanbanTasks hinzu...");
|
|
|
|
// Add NotesText column
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTasks` ADD COLUMN `NotesText` LONGTEXT NULL AFTER `ArchivedAt`;",
|
|
"AddNotesTextColumn", cancellationToken);
|
|
|
|
_logger.LogInformation("NotesText Spalte zu KanbanTasks hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für NotesText übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddIsActiveToTireStacksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'TireStacks' AND column_name = 'IsActive';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var isActiveExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!isActiveExists)
|
|
{
|
|
_logger.LogInformation("Füge IsActive Spalte zu TireStacks hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD COLUMN `IsActive` TINYINT(1) NOT NULL DEFAULT 1;",
|
|
"AddIsActiveToTireStacks", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD INDEX `IX_TireStacks_IsActive` (`IsActive`);",
|
|
"AddIsActiveIndex", cancellationToken);
|
|
|
|
_logger.LogInformation("IsActive Spalte zu TireStacks hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für TireStacks.IsActive übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateVehiclesRemoveLicensePlateAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Check if TireSize column already exists (migration already done)
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'Vehicles' AND column_name = 'TireSize';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var tireSizeExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!tireSizeExists)
|
|
{
|
|
_logger.LogInformation("Migriere Vehicles-Tabelle: Entferne LicensePlate/Year, füge TireSize hinzu...");
|
|
|
|
// Add TireSize column
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Vehicles` ADD COLUMN `TireSize` VARCHAR(50) NOT NULL DEFAULT '';",
|
|
"AddTireSizeToVehicles", cancellationToken);
|
|
|
|
// Make Make NOT NULL with default
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `Vehicles` SET `Make` = COALESCE(`Make`, 'Unbekannt') WHERE `Make` IS NULL;",
|
|
"SetDefaultMake", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Vehicles` MODIFY COLUMN `Make` VARCHAR(100) NOT NULL;",
|
|
"MakeMakeNotNull", cancellationToken);
|
|
|
|
// Drop LicensePlate index, then column
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Vehicles` DROP INDEX `IX_Vehicles_LicensePlate`;",
|
|
"DropVehicleLicensePlateIndex", cancellationToken);
|
|
|
|
await DropColumnIfExistsAsync(connection, "Vehicles", "LicensePlate", cancellationToken);
|
|
await DropColumnIfExistsAsync(connection, "Vehicles", "Year", cancellationToken);
|
|
|
|
// Add new index on Make/Model
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Vehicles` ADD INDEX `IX_Vehicles_Make_Model` (`Make`, `Model`);",
|
|
"AddVehicleMakeModelIndex", cancellationToken);
|
|
|
|
_logger.LogInformation("Vehicles-Tabelle erfolgreich migriert.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für Vehicles (LicensePlate-Entfernung) übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAdd2FaColumnsToUsersAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Users` ADD COLUMN `TwoFactorEnabled` TINYINT(1) NOT NULL DEFAULT 0;",
|
|
"AddTwoFactorEnabled", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Users` ADD COLUMN `TwoFactorSecret` VARCHAR(256) NULL;",
|
|
"AddTwoFactorSecret", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `Users` ADD COLUMN `TwoFactorEnforced` TINYINT(1) NOT NULL DEFAULT 0;",
|
|
"AddTwoFactorEnforced", cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für 2FA-Spalten übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task ExecuteSqlSafeAsync(MySqlConnection connection, string sql, string name, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var cmd = new MySqlCommand(sql, connection);
|
|
cmd.CommandTimeout = 60; // 60 second timeout for DDL operations
|
|
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
_logger.LogInformation("SQL ausgeführt: {Name}", name);
|
|
}
|
|
catch (MySqlException ex) when (ex.Number is 1091 or 1061 or 1826 or 121 or 1050 or 1025 or 1553 or 1146 or 1060 or 1068)
|
|
{
|
|
// 1091: Can't DROP - doesn't exist
|
|
// 1061: Duplicate key name
|
|
// 1826: Duplicate foreign key constraint name
|
|
// 121: Duplicate key on write or update
|
|
// 1050: Table already exists
|
|
// 1025: Error on rename (constraint doesn't exist)
|
|
// 1553: Cannot drop check constraint (doesn't exist)
|
|
// 1146: Table doesn't exist
|
|
// 1060: Duplicate column name
|
|
// 1068: Multiple primary key defined
|
|
_logger.LogDebug("SQL übersprungen (bereits vorhanden oder nicht nötig): {Name}", name);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "SQL Fehler (ignoriert): {Name}", name);
|
|
}
|
|
}
|
|
|
|
private async Task DropColumnIfExistsAsync(MySqlConnection connection, string table, string column, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var cmd = new MySqlCommand($"ALTER TABLE `{table}` DROP COLUMN `{column}`;", connection);
|
|
cmd.CommandTimeout = 60;
|
|
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
_logger.LogInformation("Spalte '{Column}' aus '{Table}' entfernt.", column, table);
|
|
}
|
|
catch
|
|
{
|
|
// Column doesn't exist - ignore
|
|
}
|
|
}
|
|
|
|
private async Task EnsureDatabaseExistsAsync(CancellationToken cancellationToken)
|
|
{
|
|
var connectionString = _configuration.GetConnectionString("DefaultConnection");
|
|
if (string.IsNullOrWhiteSpace(connectionString)) return;
|
|
|
|
var dbName = GetDatabaseName(connectionString);
|
|
if (string.IsNullOrWhiteSpace(dbName)) return;
|
|
|
|
var serverConnStr = GetServerConnectionString(connectionString);
|
|
var server = GetServerFromConnectionString(connectionString);
|
|
var port = GetPortFromConnectionString(connectionString) ?? "3306";
|
|
|
|
_logger.LogInformation("Prüfe ob Datenbank '{Database}' auf Server '{Server}:{Port}' existiert...",
|
|
dbName, server, port);
|
|
|
|
await using var connection = new MySqlConnection(serverConnStr);
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
await using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = $"CREATE DATABASE IF NOT EXISTS `{dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
|
|
cmd.CommandTimeout = 30;
|
|
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
_logger.LogInformation("Datenbank '{Database}' ist verfügbar.", dbName);
|
|
}
|
|
|
|
private static string? GetPortFromConnectionString(string? connectionString)
|
|
{
|
|
if (string.IsNullOrEmpty(connectionString)) return null;
|
|
|
|
foreach (var part in connectionString.Split(';'))
|
|
{
|
|
var kv = part.Split('=', 2);
|
|
if (kv.Length == 2 && kv[0].Trim().Equals("Port", StringComparison.OrdinalIgnoreCase))
|
|
return kv[1].Trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string? GetDatabaseName(string connectionString)
|
|
{
|
|
foreach (var part in connectionString.Split(';'))
|
|
{
|
|
var kv = part.Split('=', 2);
|
|
if (kv.Length == 2 && kv[0].Trim().Equals("Database", StringComparison.OrdinalIgnoreCase))
|
|
return kv[1].Trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string GetServerConnectionString(string connectionString)
|
|
{
|
|
var parts = connectionString.Split(';')
|
|
.Where(p => !p.Trim().StartsWith("Database=", StringComparison.OrdinalIgnoreCase));
|
|
return string.Join(";", parts);
|
|
}
|
|
|
|
private async Task MigrateAddTypeToCatalogItemsAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'CatalogItems' AND column_name = 'Type';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var typeExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!typeExists)
|
|
{
|
|
_logger.LogInformation("Füge Type Spalte zu CatalogItems hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `CatalogItems` ADD COLUMN `Type` INT NOT NULL DEFAULT 0;",
|
|
"AddTypeToCatalogItems", cancellationToken);
|
|
|
|
_logger.LogInformation("Type Spalte zu CatalogItems hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für CatalogItems.Type übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddItemCountToTireStacksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'TireStacks' AND column_name = 'ItemCount';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var itemCountExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!itemCountExists)
|
|
{
|
|
_logger.LogInformation("Füge ItemCount Spalte zu TireStacks hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `TireStacks` ADD COLUMN `ItemCount` INT NOT NULL DEFAULT 4;",
|
|
"AddItemCountToTireStacks", cancellationToken);
|
|
|
|
_logger.LogInformation("ItemCount Spalte zu TireStacks hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für TireStacks.ItemCount übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
#region SQL Statements
|
|
|
|
private const string CreateCatalogItemsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `CatalogItems` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Name` VARCHAR(200) NOT NULL,
|
|
`PictureFileName` VARCHAR(500) NULL,
|
|
`TireSize` VARCHAR(50) NOT NULL DEFAULT '',
|
|
`Season` INT NOT NULL DEFAULT 0,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateInventoryItemsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `InventoryItems` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`QrCode` VARCHAR(500) NOT NULL,
|
|
`Name` VARCHAR(200) NULL,
|
|
`Description` VARCHAR(500) NULL,
|
|
`CatalogItemId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_InventoryItems_QrCode` (`QrCode`),
|
|
INDEX `IX_InventoryItems_CatalogItemId` (`CatalogItemId`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateUsersTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `Users` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Username` VARCHAR(100) NOT NULL,
|
|
`PasswordHash` VARCHAR(256) NOT NULL,
|
|
`DisplayName` VARCHAR(200) NULL,
|
|
`Role` INT NOT NULL DEFAULT 0,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`LastLoginAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
UNIQUE INDEX `IX_Users_Username` (`Username`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateSiteSettingsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `SiteSettings` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Key` VARCHAR(100) NOT NULL,
|
|
`Value` TEXT NOT NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
UNIQUE INDEX `IX_SiteSettings_Key` (`Key`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateCustomersTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `Customers` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`CustomerNumber` VARCHAR(20) NOT NULL,
|
|
`FirstName` VARCHAR(100) NOT NULL,
|
|
`LastName` VARCHAR(100) NOT NULL,
|
|
`Company` VARCHAR(200) NULL,
|
|
`Phone` VARCHAR(20) NULL,
|
|
`Email` VARCHAR(100) NULL,
|
|
`Notes` VARCHAR(500) NULL,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
UNIQUE INDEX `IX_Customers_CustomerNumber` (`CustomerNumber`),
|
|
INDEX `IX_Customers_Name` (`LastName`, `FirstName`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateVehiclesTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `Vehicles` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`CustomerId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Make` VARCHAR(100) NOT NULL,
|
|
`Model` VARCHAR(100) NULL,
|
|
`TireSize` VARCHAR(50) NOT NULL DEFAULT '',
|
|
`Notes` VARCHAR(500) NULL,
|
|
`IsPrimary` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_Vehicles_CustomerId` (`CustomerId`),
|
|
INDEX `IX_Vehicles_Make_Model` (`Make`, `Model`),
|
|
INDEX `IX_Vehicles_CustomerId_IsPrimary` (`CustomerId`, `IsPrimary`),
|
|
CONSTRAINT `FK_Vehicles_Customers` FOREIGN KEY (`CustomerId`) REFERENCES `Customers` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
/// <summary>
|
|
/// StorageLocations table with ABCD code format:
|
|
/// - Code: 4-digit code (e.g., "2235")
|
|
/// - Area: 1=Werkstatt (3 Reihen), 2=Lager (5 Reihen)
|
|
/// - Row: Werkstatt 1-3, Lager 1-5
|
|
/// - Position: 1=links oben, 2=links unten, 3=rechts oben, 4=rechts unten
|
|
/// - SlotNumber: 1-9 (längs)
|
|
/// - Each location holds exactly one tire stack
|
|
/// </summary>
|
|
private const string CreateStorageLocationsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `StorageLocations` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Code` VARCHAR(4) NOT NULL,
|
|
`Area` INT NOT NULL DEFAULT 1,
|
|
`Row` INT NOT NULL DEFAULT 1,
|
|
`Position` INT NOT NULL DEFAULT 1,
|
|
`SlotNumber` INT NOT NULL DEFAULT 1,
|
|
`IsOccupied` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`TireStackId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`OccupiedSince` DATETIME(6) NULL,
|
|
`Notes` VARCHAR(500) NULL,
|
|
`IsAvailable` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
UNIQUE INDEX `IX_StorageLocations_Code` (`Code`),
|
|
INDEX `IX_StorageLocations_Area_Row` (`Area`, `Row`),
|
|
UNIQUE INDEX `IX_StorageLocations_Area_Row_Position_Slot` (`Area`, `Row`, `Position`, `SlotNumber`),
|
|
INDEX `IX_StorageLocations_IsOccupied` (`IsOccupied`),
|
|
INDEX `IX_StorageLocations_TireStackId` (`TireStackId`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateTireStacksTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `TireStacks` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TicketNumber` VARCHAR(50) NOT NULL,
|
|
`CustomerId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`VehicleId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`StorageLocationId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`CatalogItemId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`Status` INT NOT NULL DEFAULT 0,
|
|
`TireSize` VARCHAR(50) NULL,
|
|
`TireBrand` VARCHAR(100) NULL,
|
|
`TireModel` VARCHAR(100) NULL,
|
|
`Season` INT NOT NULL DEFAULT 0,
|
|
`TreadDepth` DECIMAL(4,1) NULL,
|
|
`DotCode` VARCHAR(20) NULL,
|
|
`WithRims` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`RimSize` VARCHAR(20) NULL,
|
|
`HasTpms` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`Condition` INT NOT NULL DEFAULT 1,
|
|
`Notes` VARCHAR(1000) NULL,
|
|
`StoredAt` DATETIME(6) NOT NULL,
|
|
`PickedUpAt` DATETIME(6) NULL,
|
|
`ExpectedPickupDate` DATETIME(6) NULL,
|
|
`StoredByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`PickedUpByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
UNIQUE INDEX `IX_TireStacks_TicketNumber` (`TicketNumber`),
|
|
INDEX `IX_TireStacks_Status` (`Status`),
|
|
INDEX `IX_TireStacks_CustomerId` (`CustomerId`),
|
|
INDEX `IX_TireStacks_VehicleId` (`VehicleId`),
|
|
INDEX `IX_TireStacks_StorageLocationId` (`StorageLocationId`),
|
|
CONSTRAINT `FK_TireStacks_Customers` FOREIGN KEY (`CustomerId`) REFERENCES `Customers` (`Id`) ON DELETE RESTRICT,
|
|
CONSTRAINT `FK_TireStacks_Vehicles` FOREIGN KEY (`VehicleId`) REFERENCES `Vehicles` (`Id`) ON DELETE SET NULL,
|
|
CONSTRAINT `FK_TireStacks_StorageLocations` FOREIGN KEY (`StorageLocationId`) REFERENCES `StorageLocations` (`Id`) ON DELETE SET NULL,
|
|
CONSTRAINT `FK_TireStacks_CatalogItems` FOREIGN KEY (`CatalogItemId`) REFERENCES `CatalogItems` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateTireItemsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `TireItems` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TireStackId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Position` INT NOT NULL DEFAULT 0,
|
|
`TreadDepth` DECIMAL(4,1) NULL,
|
|
`Condition` INT NOT NULL DEFAULT 1,
|
|
`DamageNotes` VARCHAR(500) NULL,
|
|
`DotCode` VARCHAR(20) NULL,
|
|
`PhotoFileName` VARCHAR(500) NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_TireItems_TireStackId` (`TireStackId`),
|
|
CONSTRAINT `FK_TireItems_TireStacks` FOREIGN KEY (`TireStackId`) REFERENCES `TireStacks` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateTireInspectionsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `TireInspections` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TireStackId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Source` VARCHAR(50) NOT NULL DEFAULT 'Launch TTS01',
|
|
`RecordSn` VARCHAR(100) NULL,
|
|
`ReportUrl` VARCHAR(500) NULL,
|
|
`MeasuredAt` DATETIME(6) NULL,
|
|
`LicensePlate` VARCHAR(20) NULL,
|
|
`Mileage` INT NULL,
|
|
`TreadDepthFL` DECIMAL(5,2) NULL,
|
|
`TreadDepthFR` DECIMAL(5,2) NULL,
|
|
`TreadDepthRL` DECIMAL(5,2) NULL,
|
|
`TreadDepthRR` DECIMAL(5,2) NULL,
|
|
`RatingFL` INT NOT NULL DEFAULT 0,
|
|
`RatingFR` INT NOT NULL DEFAULT 0,
|
|
`RatingRL` INT NOT NULL DEFAULT 0,
|
|
`RatingRR` INT NOT NULL DEFAULT 0,
|
|
`Recommendation` VARCHAR(1000) NULL,
|
|
`RawJson` LONGTEXT NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`CreatedByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_TireInspections_TireStackId` (`TireStackId`),
|
|
INDEX `IX_TireInspections_RecordSn` (`RecordSn`),
|
|
CONSTRAINT `FK_TireInspections_TireStacks` FOREIGN KEY (`TireStackId`) REFERENCES `TireStacks` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
#region Kanban SQL Statements
|
|
|
|
private const string CreateKanbanColumnsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanColumns` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Name` VARCHAR(100) NOT NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`Color` VARCHAR(7) NULL,
|
|
`Icon` VARCHAR(10) NULL,
|
|
`IsDefault` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`IsCompletedColumn` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanColumns_SortOrder` (`SortOrder`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanLabelsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanLabels` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Name` VARCHAR(50) NOT NULL,
|
|
`Color` VARCHAR(7) NOT NULL DEFAULT '#6B7280',
|
|
`Description` VARCHAR(200) NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanLabels_SortOrder` (`SortOrder`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTasksTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTasks` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Title` VARCHAR(200) NOT NULL,
|
|
`Description` LONGTEXT NULL,
|
|
`ColumnId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`DueDate` DATETIME(6) NULL,
|
|
`Priority` INT NOT NULL DEFAULT 1,
|
|
`EstimatedMinutes` INT NULL,
|
|
`CreatedByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`IsArchived` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`CompletedAt` DATETIME(6) NULL,
|
|
`ArchivedAt` DATETIME(6) NULL,
|
|
`NotesText` LONGTEXT NULL,
|
|
`TireSizeNote` VARCHAR(100) NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTasks_ColumnId` (`ColumnId`),
|
|
INDEX `IX_KanbanTasks_DueDate` (`DueDate`),
|
|
INDEX `IX_KanbanTasks_IsArchived` (`IsArchived`),
|
|
INDEX `IX_KanbanTasks_ColumnId_SortOrder` (`ColumnId`, `SortOrder`),
|
|
CONSTRAINT `FK_KanbanTasks_KanbanColumns` FOREIGN KEY (`ColumnId`) REFERENCES `KanbanColumns` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTasks_Users` FOREIGN KEY (`CreatedByUserId`) REFERENCES `Users` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskTodosTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskTodos` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Text` VARCHAR(500) NOT NULL,
|
|
`IsCompleted` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`CompletedAt` DATETIME(6) NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTaskTodos_TaskId` (`TaskId`),
|
|
CONSTRAINT `FK_KanbanTaskTodos_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskCommentsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskComments` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`UserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`Content` LONGTEXT NOT NULL,
|
|
`CommentType` INT NOT NULL DEFAULT 0,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTaskComments_TaskId` (`TaskId`),
|
|
INDEX `IX_KanbanTaskComments_CreatedAt` (`CreatedAt`),
|
|
CONSTRAINT `FK_KanbanTaskComments_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskComments_Users` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskNotesTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskNotes` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Title` VARCHAR(100) NOT NULL DEFAULT 'Notizen',
|
|
`Content` LONGTEXT NOT NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`LastEditedByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTaskNotes_TaskId` (`TaskId`),
|
|
CONSTRAINT `FK_KanbanTaskNotes_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskNotes_Users` FOREIGN KEY (`LastEditedByUserId`) REFERENCES `Users` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskAttachmentsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskAttachments` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`FileName` VARCHAR(255) NOT NULL,
|
|
`ContentType` VARCHAR(100) NOT NULL DEFAULT 'application/octet-stream',
|
|
`FileSize` BIGINT NOT NULL DEFAULT 0,
|
|
`FilePath` VARCHAR(500) NULL,
|
|
`FileContent` LONGBLOB NULL,
|
|
`UploadedByUserId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTaskAttachments_TaskId` (`TaskId`),
|
|
CONSTRAINT `FK_KanbanTaskAttachments_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskAttachments_Users` FOREIGN KEY (`UploadedByUserId`) REFERENCES `Users` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskTemplatesTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskTemplates` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Name` VARCHAR(100) NOT NULL,
|
|
`Description` VARCHAR(500) NULL,
|
|
`TitlePattern` VARCHAR(200) NOT NULL DEFAULT '',
|
|
`DefaultDescription` LONGTEXT NULL,
|
|
`DefaultColumnId` CHAR(36) CHARACTER SET ascii NULL,
|
|
`DefaultPriority` INT NOT NULL DEFAULT 1,
|
|
`DefaultEstimatedMinutes` INT NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
`IsActive` TINYINT(1) NOT NULL DEFAULT 1,
|
|
`CreatedAt` DATETIME(6) NOT NULL,
|
|
`UpdatedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`Id`),
|
|
INDEX `IX_KanbanTaskTemplates_SortOrder` (`SortOrder`),
|
|
CONSTRAINT `FK_KanbanTaskTemplates_KanbanColumns` FOREIGN KEY (`DefaultColumnId`) REFERENCES `KanbanColumns` (`Id`) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskTemplateTodosTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskTemplateTodos` (
|
|
`Id` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TemplateId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`Text` VARCHAR(500) NOT NULL,
|
|
`SortOrder` INT NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`Id`),
|
|
CONSTRAINT `FK_KanbanTaskTemplateTodos_KanbanTaskTemplates` FOREIGN KEY (`TemplateId`) REFERENCES `KanbanTaskTemplates` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskLabelsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskLabels` (
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`LabelId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
PRIMARY KEY (`TaskId`, `LabelId`),
|
|
CONSTRAINT `FK_KanbanTaskLabels_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskLabels_KanbanLabels` FOREIGN KEY (`LabelId`) REFERENCES `KanbanLabels` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskTemplateLabelsTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskTemplateLabels` (
|
|
`TemplateId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`LabelId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
PRIMARY KEY (`TemplateId`, `LabelId`),
|
|
CONSTRAINT `FK_KanbanTaskTemplateLabels_KanbanTaskTemplates` FOREIGN KEY (`TemplateId`) REFERENCES `KanbanTaskTemplates` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskTemplateLabels_KanbanLabels` FOREIGN KEY (`LabelId`) REFERENCES `KanbanLabels` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskAssigneesTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskAssignees` (
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`UserId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`AssignedAt` DATETIME(6) NOT NULL,
|
|
PRIMARY KEY (`TaskId`, `UserId`),
|
|
CONSTRAINT `FK_KanbanTaskAssignees_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskAssignees_Users` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskCustomersTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskCustomers` (
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`CustomerId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
PRIMARY KEY (`TaskId`, `CustomerId`),
|
|
CONSTRAINT `FK_KanbanTaskCustomers_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskCustomers_Customers` FOREIGN KEY (`CustomerId`) REFERENCES `Customers` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private const string CreateKanbanTaskTireStacksTableSql = """
|
|
CREATE TABLE IF NOT EXISTS `KanbanTaskTireStacks` (
|
|
`TaskId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
`TireStackId` CHAR(36) CHARACTER SET ascii NOT NULL,
|
|
PRIMARY KEY (`TaskId`, `TireStackId`),
|
|
CONSTRAINT `FK_KanbanTaskTireStacks_KanbanTasks` FOREIGN KEY (`TaskId`) REFERENCES `KanbanTasks` (`Id`) ON DELETE CASCADE,
|
|
CONSTRAINT `FK_KanbanTaskTireStacks_TireStacks` FOREIGN KEY (`TireStackId`) REFERENCES `TireStacks` (`Id`) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
""";
|
|
|
|
private async Task MigrateAddTireSizeNoteToKanbanTasksAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'KanbanTasks' AND column_name = 'TireSizeNote';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var tireSizeNoteExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!tireSizeNoteExists)
|
|
{
|
|
_logger.LogInformation("Füge TireSizeNote Spalte zu KanbanTasks hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTasks` ADD COLUMN `TireSizeNote` VARCHAR(100) NULL AFTER `NotesText`;",
|
|
"AddTireSizeNoteColumn", cancellationToken);
|
|
|
|
_logger.LogInformation("TireSizeNote Spalte zu KanbanTasks hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für TireSizeNote übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateRenameKanbanWaitColumnAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"UPDATE `KanbanColumns` SET `Name` = 'Warte auf Kunden', `Icon` = '\u23F3' WHERE `Name` = 'Warte auf wen anders';",
|
|
"RenameKanbanWaitColumn", cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Fehler beim Umbenennen der Kanban-Spalte.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateFixKanbanColumnIconsAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Fix icons that were corrupted due to encoding issues in original seed data
|
|
var iconFixes = new[]
|
|
{
|
|
(Name: "Backlog", Icon: "\U0001F4E5"),
|
|
(Name: "ToDo", Icon: "\U0001F4CB"),
|
|
(Name: "In Arbeit", Icon: "\U0001F504"),
|
|
(Name: "Warte auf Kunden", Icon: "\u23F3"),
|
|
(Name: "Erledigt", Icon: "\u2705")
|
|
};
|
|
|
|
foreach (var fix in iconFixes)
|
|
{
|
|
await using var cmd = new MySqlCommand(
|
|
"UPDATE `KanbanColumns` SET `Icon` = @icon WHERE `Name` = @name AND (`Icon` IS NULL OR `Icon` = '' OR `Icon` LIKE '%?%');",
|
|
connection);
|
|
cmd.CommandTimeout = 30;
|
|
cmd.Parameters.AddWithValue("@icon", fix.Icon);
|
|
cmd.Parameters.AddWithValue("@name", fix.Name);
|
|
var affected = await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
if (affected > 0)
|
|
{
|
|
_logger.LogInformation("Kanban-Spalte '{Name}' Icon korrigiert.", fix.Name);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für Kanban-Spalten-Icons übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task MigrateAddVehicleIdToKanbanTaskCustomersAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand(
|
|
"SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'KanbanTaskCustomers' AND column_name = 'VehicleId';",
|
|
connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var vehicleIdExists = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken)) > 0;
|
|
|
|
if (!vehicleIdExists)
|
|
{
|
|
_logger.LogInformation("Füge VehicleId Spalte zu KanbanTaskCustomers hinzu...");
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTaskCustomers` ADD COLUMN `VehicleId` CHAR(36) CHARACTER SET ascii NULL;",
|
|
"AddVehicleIdToKanbanTaskCustomers", cancellationToken);
|
|
|
|
await ExecuteSqlSafeAsync(connection,
|
|
"ALTER TABLE `KanbanTaskCustomers` ADD CONSTRAINT `FK_KanbanTaskCustomers_Vehicles` FOREIGN KEY (`VehicleId`) REFERENCES `Vehicles` (`Id`) ON DELETE SET NULL;",
|
|
"AddVehicleIdFK", cancellationToken);
|
|
|
|
_logger.LogInformation("VehicleId Spalte zu KanbanTaskCustomers hinzugefügt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Migration für VehicleId in KanbanTaskCustomers übersprungen oder fehlgeschlagen.");
|
|
}
|
|
}
|
|
|
|
private async Task InitializeDefaultKanbanColumnsAsync(MySqlConnection connection, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await using var checkCmd = new MySqlCommand("SELECT COUNT(*) FROM `KanbanColumns`;", connection);
|
|
checkCmd.CommandTimeout = 30;
|
|
var count = Convert.ToInt32(await checkCmd.ExecuteScalarAsync(cancellationToken));
|
|
|
|
if (count == 0)
|
|
{
|
|
_logger.LogInformation("Erstelle Standard-Kanban-Spalten...");
|
|
|
|
var columns = new[]
|
|
{
|
|
(Name: "Backlog", Order: 0, Icon: "\U0001F4E5", IsDefault: true, IsCompleted: false),
|
|
(Name: "ToDo", Order: 1, Icon: "\U0001F4CB", IsDefault: false, IsCompleted: false),
|
|
(Name: "In Arbeit", Order: 2, Icon: "\U0001F504", IsDefault: false, IsCompleted: false),
|
|
(Name: "Warte auf Kunden", Order: 3, Icon: "\u23F3", IsDefault: false, IsCompleted: false),
|
|
(Name: "Erledigt", Order: 4, Icon: "\u2705", IsDefault: false, IsCompleted: true)
|
|
};
|
|
|
|
foreach (var col in columns)
|
|
{
|
|
var id = Guid.NewGuid().ToString();
|
|
var now = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.ffffff");
|
|
var sql = $"""
|
|
INSERT INTO `KanbanColumns` (`Id`, `Name`, `SortOrder`, `Icon`, `IsDefault`, `IsCompletedColumn`, `IsActive`, `CreatedAt`, `UpdatedAt`)
|
|
VALUES ('{id}', '{col.Name}', {col.Order}, '{col.Icon}', {(col.IsDefault ? 1 : 0)}, {(col.IsCompleted ? 1 : 0)}, 1, '{now}', '{now}');
|
|
""";
|
|
await ExecuteSqlSafeAsync(connection, sql, $"InsertColumn_{col.Name}", cancellationToken);
|
|
}
|
|
|
|
_logger.LogInformation("Standard-Kanban-Spalten erstellt.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Fehler beim Erstellen der Standard-Kanban-Spalten.");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
}
|