Remove ShopWorker entity and migrate worker identity to ApplicationUser
Removes the ShopWorker and ShopWorkerRoleCost entities, all related DTOs, mappings, controllers, views, and import/export paths. Worker identity is now handled entirely through ApplicationUser with per-user LaborCostPerHour. ShopWorkerRoleCosts table remains in production pending manual data migration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,11 +205,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
public DbSet<MaintenanceRecord> MaintenanceRecords { get; set; }
|
||||
/// <summary>Supplier/vendor records used by Purchasing and Accounts Payable; tenant-filtered with soft delete.</summary>
|
||||
public DbSet<Vendor> Vendors { get; set; }
|
||||
/// <summary>Shop worker profiles with role assignments; tenant-filtered with soft delete.</summary>
|
||||
public DbSet<ShopWorker> ShopWorkers { get; set; }
|
||||
/// <summary>Per-role labour cost rates used in pricing calculations; unique index on (CompanyId, Role).</summary>
|
||||
public DbSet<ShopWorkerRoleCost> ShopWorkerRoleCosts { get; set; }
|
||||
/// <summary>Rework records tracking quality failures and remediation work against a job; tenant-filtered with soft delete.</summary>
|
||||
/// <summary>Rework records tracking quality failures and remediation work against a job; tenant-filtered with soft delete.</summary>
|
||||
public DbSet<ReworkRecord> ReworkRecords { get; set; }
|
||||
/// <summary>Customer refund records; tenant-filtered with soft delete.</summary>
|
||||
public DbSet<Refund> Refunds { get; set; }
|
||||
@@ -530,11 +526,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<JobStatusHistory>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<ShopWorker>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<ShopWorkerRoleCost>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<ReworkRecord>().HasQueryFilter(e =>
|
||||
modelBuilder.Entity<ReworkRecord>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
modelBuilder.Entity<Refund>().HasQueryFilter(e =>
|
||||
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
|
||||
@@ -1314,12 +1306,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
.HasForeignKey(m => m.PerformedById)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// ShopWorker relationships
|
||||
modelBuilder.Entity<ShopWorker>()
|
||||
.HasOne<Company>()
|
||||
.WithMany(c => c.ShopWorkers)
|
||||
.HasForeignKey(e => e.CompanyId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasOne(j => j.AssignedUser)
|
||||
@@ -1393,10 +1380,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
modelBuilder.Entity<PricingTier>()
|
||||
.HasIndex(p => p.CompanyId);
|
||||
|
||||
modelBuilder.Entity<ShopWorker>()
|
||||
.HasIndex(w => w.CompanyId);
|
||||
|
||||
modelBuilder.Entity<CatalogCategory>()
|
||||
modelBuilder.Entity<CatalogCategory>()
|
||||
.HasIndex(c => c.CompanyId);
|
||||
|
||||
modelBuilder.Entity<CatalogCategory>()
|
||||
@@ -1431,12 +1415,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Jobs_CompanyId_JobNumber");
|
||||
|
||||
modelBuilder.Entity<ShopWorkerRoleCost>()
|
||||
.HasIndex(r => new { r.CompanyId, r.Role })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_ShopWorkerRoleCosts_CompanyId_Role");
|
||||
|
||||
modelBuilder.Entity<Job>()
|
||||
modelBuilder.Entity<Job>()
|
||||
.Property(j => j.ShopAccessCode)
|
||||
.HasDefaultValueSql("NEWID()");
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public class AuditInterceptor : SaveChangesInterceptor
|
||||
private static readonly HashSet<string> AuditedTypes = new(StringComparer.Ordinal)
|
||||
{
|
||||
nameof(Customer), nameof(Job), nameof(Quote), nameof(Equipment),
|
||||
nameof(MaintenanceRecord), nameof(Vendor), nameof(ShopWorker),
|
||||
nameof(MaintenanceRecord), nameof(Vendor),
|
||||
nameof(InventoryItem), nameof(Company),
|
||||
// Financial entities
|
||||
nameof(Invoice), nameof(Payment), nameof(Bill), nameof(BillPayment),
|
||||
|
||||
Generated
+10784
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLaborCostPerHour : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "LaborCostPerHour",
|
||||
table: "CompanyOperatingCosts",
|
||||
type: "decimal(18,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "LaborCostPerHour",
|
||||
table: "AspNetUsers",
|
||||
type: "decimal(18,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3131));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3137));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3138));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LaborCostPerHour",
|
||||
table: "CompanyOperatingCosts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LaborCostPerHour",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 1,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(845));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 2,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(850));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "PricingTiers",
|
||||
keyColumn: "Id",
|
||||
keyValue: 3,
|
||||
column: "CreatedAt",
|
||||
value: new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(852));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,6 +556,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<decimal?>("LaborCostPerHour")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime?>("LastLoginDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
@@ -2075,6 +2078,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<decimal?>("LaborCostPerHour")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("MonthlyBillableHours")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -6714,7 +6720,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 1,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(845),
|
||||
CreatedAt = new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3131),
|
||||
Description = "Standard pricing for regular customers",
|
||||
DiscountPercent = 0m,
|
||||
IsActive = true,
|
||||
@@ -6725,7 +6731,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 2,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(850),
|
||||
CreatedAt = new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3137),
|
||||
Description = "5% discount for preferred customers",
|
||||
DiscountPercent = 5m,
|
||||
IsActive = true,
|
||||
@@ -6736,7 +6742,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
||||
{
|
||||
Id = 3,
|
||||
CompanyId = 0,
|
||||
CreatedAt = new DateTime(2026, 5, 15, 19, 43, 40, 586, DateTimeKind.Utc).AddTicks(852),
|
||||
CreatedAt = new DateTime(2026, 5, 15, 23, 44, 10, 471, DateTimeKind.Utc).AddTicks(3138),
|
||||
Description = "10% discount for premium customers",
|
||||
DiscountPercent = 10m,
|
||||
IsActive = true,
|
||||
|
||||
@@ -171,7 +171,6 @@ public class JobRepository : Repository<Job>, IJobRepository
|
||||
.Include(j => j.JobItems.Where(i => !i.IsDeleted))
|
||||
.ThenInclude(i => i.Coats.Where(c => !c.IsDeleted))
|
||||
.Include(j => j.TimeEntries.Where(t => !t.IsDeleted))
|
||||
.ThenInclude(t => t.Worker)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ public class UnitOfWork : IUnitOfWork
|
||||
private IRepository<AppointmentStatusLookup>? _appointmentStatusLookups;
|
||||
private IRepository<AppointmentTypeLookup>? _appointmentTypeLookups;
|
||||
private IRepository<PrepService>? _prepServices;
|
||||
private IRepository<ShopWorker>? _shopWorkers;
|
||||
|
||||
// Appointments
|
||||
private IRepository<Appointment>? _appointments;
|
||||
@@ -350,16 +349,7 @@ public class UnitOfWork : IUnitOfWork
|
||||
public IRepository<PrepService> PrepServices =>
|
||||
_prepServices ??= new Repository<PrepService>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="ShopWorker"/> profiles with role assignments; tenant-filtered with soft delete.</summary>
|
||||
public IRepository<ShopWorker> ShopWorkers =>
|
||||
_shopWorkers ??= new Repository<ShopWorker>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="ShopWorkerRoleCost"/> per-role labour cost rates; unique on (CompanyId, Role).</summary>
|
||||
private IRepository<ShopWorkerRoleCost>? _shopWorkerRoleCosts;
|
||||
public IRepository<ShopWorkerRoleCost> ShopWorkerRoleCosts =>
|
||||
_shopWorkerRoleCosts ??= new Repository<ShopWorkerRoleCost>(_context);
|
||||
|
||||
/// <summary>Repository for <see cref="ReworkRecord"/> quality-failure and remediation records; tenant-filtered with soft delete.</summary>
|
||||
/// <summary>Repository for <see cref="ReworkRecord"/> quality-failure and remediation records; tenant-filtered with soft delete.</summary>
|
||||
private IRepository<ReworkRecord>? _reworkRecords;
|
||||
public IRepository<ReworkRecord> ReworkRecords =>
|
||||
_reworkRecords ??= new Repository<ReworkRecord>(_context);
|
||||
|
||||
@@ -73,7 +73,6 @@ public class CompanyDataPurgeService : ICompanyDataPurgeService
|
||||
await _context.NotificationTemplates.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.Announcements.Where(x => x.TargetCompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.BugReports.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkers.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.PrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
|
||||
// ── Tier 4: Company configs and lookup tables ─────────────────────────
|
||||
@@ -137,7 +136,6 @@ public class CompanyDataPurgeService : ICompanyDataPurgeService
|
||||
await _context.PurchaseOrderItems.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.AiItemPredictions.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.PowderUsageLogs.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkerRoleCosts.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.OvenBatches.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.Refunds.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.CreditMemos.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
@@ -160,7 +158,6 @@ public class CompanyDataPurgeService : ICompanyDataPurgeService
|
||||
await _context.OvenCosts.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.Accounts.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.NotificationLogs.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.ShopWorkers.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.PrepServices.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.CatalogCategories.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
await _context.InventoryCategoryLookups.IgnoreQueryFilters().Where(x => x.CompanyId == companyId).ExecuteDeleteAsync();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
@@ -2164,168 +2164,6 @@ public class CsvImportService : ICsvImportService
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shop Worker Import
|
||||
|
||||
/// <summary>
|
||||
/// Generates a downloadable CSV template with two example shop worker rows covering different roles.
|
||||
/// Two rows help users see how Role values (Coater, Sandblaster, etc.) are expressed and remind
|
||||
/// them that Role is optional — the importer will default to GeneralLabor when it is omitted.
|
||||
/// </summary>
|
||||
public byte[] GenerateShopWorkerTemplate()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var writer = new StreamWriter(memoryStream);
|
||||
using var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture));
|
||||
|
||||
csv.WriteHeader<ShopWorkerImportDto>();
|
||||
csv.NextRecord();
|
||||
|
||||
csv.WriteRecord(new ShopWorkerImportDto
|
||||
{
|
||||
Name = "John Doe",
|
||||
Role = "Coater",
|
||||
Phone = "555-1234",
|
||||
Email = "johndoe@example.com",
|
||||
IsActive = true,
|
||||
Notes = "Experienced powder coater"
|
||||
});
|
||||
csv.NextRecord();
|
||||
|
||||
csv.WriteRecord(new ShopWorkerImportDto
|
||||
{
|
||||
Name = "Jane Smith",
|
||||
Role = "Sandblaster",
|
||||
Phone = "555-5678",
|
||||
Email = "janesmith@example.com",
|
||||
IsActive = true,
|
||||
Notes = ""
|
||||
});
|
||||
csv.NextRecord();
|
||||
|
||||
writer.Flush();
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports shop workers from a CSV stream using an upsert strategy keyed on worker Name.
|
||||
/// Like vendor import, this is intentionally an upsert rather than insert-only so that a
|
||||
/// company can re-import their HR list to update phone/email/role details without worrying
|
||||
/// about creating duplicates. Role is parsed case-insensitively with spaces stripped so that
|
||||
/// "General Labor" and "GeneralLabor" are both accepted; an unrecognised role falls back to
|
||||
/// GeneralLabor with a warning rather than failing the row.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">Readable stream of CSV data (header row required).</param>
|
||||
/// <param name="companyId">Tenant company that will own newly inserted worker records.</param>
|
||||
public async Task<CsvImportResultDto> ImportShopWorkersAsync(Stream csvStream, int companyId)
|
||||
{
|
||||
var result = new CsvImportResultDto();
|
||||
var rowNumber = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(csvStream);
|
||||
using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture)
|
||||
{
|
||||
HeaderValidated = null,
|
||||
MissingFieldFound = null
|
||||
});
|
||||
|
||||
var records = csv.GetRecords<ShopWorkerImportDto>().ToList();
|
||||
result.TotalRows = records.Count;
|
||||
|
||||
_logger.LogInformation("Starting import of {Count} shop workers for company {CompanyId}", records.Count, companyId);
|
||||
|
||||
// Load existing workers for upsert matching by name
|
||||
var existingWorkers = await _unitOfWork.ShopWorkers.GetAllAsync();
|
||||
var workerDict = existingWorkers
|
||||
.Where(w => !string.IsNullOrEmpty(w.Name))
|
||||
.GroupBy(w => w.Name.Trim().ToUpperInvariant())
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
rowNumber++;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(record.Name))
|
||||
{
|
||||
result.Errors.Add($"Row {rowNumber}: Name is required.");
|
||||
result.ErrorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse role
|
||||
ShopWorkerRole role = ShopWorkerRole.GeneralLabor;
|
||||
if (!string.IsNullOrEmpty(record.Role))
|
||||
{
|
||||
if (!Enum.TryParse<ShopWorkerRole>(record.Role.Replace(" ", ""), true, out role))
|
||||
{
|
||||
result.Warnings.Add($"Row {rowNumber}: Role '{record.Role}' not recognized. Valid values: GeneralLabor, Sandblaster, Coater, Masker, QualityControl, OvenOperator, Supervisor, Maintenance. Using 'GeneralLabor'.");
|
||||
role = ShopWorkerRole.GeneralLabor;
|
||||
}
|
||||
}
|
||||
|
||||
var key = record.Name.Trim().ToUpperInvariant();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (workerDict.TryGetValue(key, out var existing))
|
||||
{
|
||||
// Update
|
||||
existing.Role = role;
|
||||
existing.Phone = record.Phone ?? existing.Phone;
|
||||
existing.Email = record.Email ?? existing.Email;
|
||||
if (record.IsActive.HasValue) existing.IsActive = record.IsActive.Value;
|
||||
existing.Notes = record.Notes ?? existing.Notes;
|
||||
existing.UpdatedAt = now;
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
result.Warnings.Add($"Row {rowNumber}: Updated existing shop worker '{record.Name}'.");
|
||||
result.SuccessCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var worker = new Core.Entities.ShopWorker
|
||||
{
|
||||
CompanyId = companyId,
|
||||
Name = record.Name.Trim(),
|
||||
Role = role,
|
||||
Phone = record.Phone,
|
||||
Email = record.Email,
|
||||
IsActive = record.IsActive ?? true,
|
||||
Notes = record.Notes,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
await _unitOfWork.ShopWorkers.AddAsync(worker);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
result.SuccessCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add($"Row {rowNumber}: Database error - {ex.Message}");
|
||||
result.ErrorCount++;
|
||||
_logger.LogError(ex, "Error saving shop worker at row {RowNumber}", rowNumber);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Shop worker import completed: {SuccessCount} succeeded, {ErrorCount} failed", result.SuccessCount, result.ErrorCount);
|
||||
result.Success = result.SuccessCount > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Errors.Add($"Fatal error: {ex.Message}");
|
||||
result.Success = false;
|
||||
_logger.LogError(ex, "Fatal error importing shop workers");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prep Service Import
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user