using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Api.SeaHavenIndustries.Controllers { [Authorize] [ApiController] [Route("api/[controller]")] public class SyncController : Controller { private readonly ApplicationDbContext _db; private readonly AmazonDynamoDBClient _dynamo; private const string WO_TABLE = "WorkOrders"; private const string COMMENTS_TABLE = "WorkOrderComments"; public SyncController(ApplicationDbContext db) { _db = db; _dynamo = new AmazonDynamoDBClient(Amazon.RegionEndpoint.USEast1); } [HttpPost("WorkOrders")] public async Task SyncWorkOrders() { var synced = 0; var created = 0; var updated = 0; var locationsCreated = 0; Dictionary? lastKey = null; do { var request = new ScanRequest { TableName = WO_TABLE, Limit = 100, ExclusiveStartKey = lastKey }; var response = await _dynamo.ScanAsync(request); foreach (var item in response.Items) { var externalId = GetString(item, "work_order_id"); if (string.IsNullOrEmpty(externalId)) continue; var existing = await _db.workOrders .FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalId); var locationId = await ResolveLocationId( GetString(item, "site_code"), GetString(item, "building"), GetString(item, "address")); if (locationId.HasValue && !locationsCreated.Equals(0)) locationsCreated++; if (existing == null) { var wo = new WorkOrder { ExternalWorkOrderId = externalId, WorkerOrderNumber = externalId, WorkerOrderTitle = GetString(item, "description"), Description = GetString(item, "description"), Status = MapStatus(GetString(item, "wo_status")), Priority = MapSeverityToPriority(GetString(item, "severity")), Severity = GetString(item, "severity"), Customer = GetString(item, "customer"), SiteCode = GetString(item, "site_code"), Building = GetString(item, "building"), LocationId = locationId, DueDate = ParseDate(GetString(item, "due_date")), DateReported = ParseDate(GetString(item, "date_reported")), ScheduledStart = ParseDate(GetString(item, "scheduled_start")), AssignTo = null, SourceEmailS3Key = GetString(item, "source_email_s3_key"), CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow, istemplate = false }; _db.workOrders.Add(wo); created++; } else { existing.WorkerOrderTitle = GetString(item, "description") ?? existing.WorkerOrderTitle; existing.Description = GetString(item, "description") ?? existing.Description; existing.Status = MapStatus(GetString(item, "wo_status")) ?? existing.Status; existing.Priority = MapSeverityToPriority(GetString(item, "severity")) ?? existing.Priority; existing.Severity = GetString(item, "severity") ?? existing.Severity; existing.SiteCode = GetString(item, "site_code") ?? existing.SiteCode; existing.Building = GetString(item, "building") ?? existing.Building; existing.LocationId = locationId ?? existing.LocationId; existing.DueDate = ParseDate(GetString(item, "due_date")) ?? existing.DueDate; existing.DateReported = ParseDate(GetString(item, "date_reported")) ?? existing.DateReported; existing.ScheduledStart = ParseDate(GetString(item, "scheduled_start")) ?? existing.ScheduledStart; existing.SourceEmailS3Key = GetString(item, "source_email_s3_key") ?? existing.SourceEmailS3Key; updated++; } synced++; if (synced % 100 == 0) await _db.SaveChangesAsync(); } lastKey = response.LastEvaluatedKey; } while (lastKey != null && lastKey.Count > 0); await _db.SaveChangesAsync(); return Ok(new { synced, created, updated, locationsCreated = await _db.Locations.CountAsync() }); } [HttpPost("Comments")] public async Task SyncComments() { var synced = 0; var created = 0; var skipped = 0; Dictionary? lastKey = null; do { var request = new ScanRequest { TableName = COMMENTS_TABLE, Limit = 100, ExclusiveStartKey = lastKey }; var response = await _dynamo.ScanAsync(request); foreach (var item in response.Items) { var externalCommentId = GetString(item, "comment_id"); var externalWoId = GetString(item, "work_order_id"); if (string.IsNullOrEmpty(externalCommentId) || string.IsNullOrEmpty(externalWoId)) continue; var alreadyExists = await _db.Comments .AnyAsync(c => c.ExternalCommentId == externalCommentId); if (alreadyExists) { skipped++; synced++; continue; } var workOrder = await _db.workOrders .FirstOrDefaultAsync(w => w.ExternalWorkOrderId == externalWoId); if (workOrder == null) { skipped++; continue; } var comment = new Comments { ExternalCommentId = externalCommentId, WorkerOrderId = workOrder.Id, Commenttext = GetString(item, "text"), Commenter = GetString(item, "commenter"), RecordType = GetString(item, "record_type"), CreatedDate = ParseDate(GetString(item, "created_at")) ?? DateTime.UtcNow, }; _db.Comments.Add(comment); created++; synced++; if (synced % 100 == 0) await _db.SaveChangesAsync(); } lastKey = response.LastEvaluatedKey; } while (lastKey != null && lastKey.Count > 0); await _db.SaveChangesAsync(); return Ok(new { synced, created, skipped }); } [HttpPost("All")] public async Task SyncAll() { var woResult = await SyncWorkOrders() as OkObjectResult; var commentResult = await SyncComments() as OkObjectResult; return Ok(new { workOrders = woResult?.Value, comments = commentResult?.Value }); } private async Task ResolveLocationId(string? siteCode, string? building, string? address) { if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building)) return null; var matchCode = siteCode ?? building; var existing = await _db.Locations .FirstOrDefaultAsync(l => l.Name == matchCode || l.Title == matchCode); if (existing != null) return existing.Id; var location = new Locations { Name = matchCode, Title = building, Address = address, Status = "Active" }; _db.Locations.Add(location); await _db.SaveChangesAsync(); return location.Id; } private static string? GetString(Dictionary item, string key) { if (item.TryGetValue(key, out var val) && val.S != null) return val.S; return null; } private static DateTime? ParseDate(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; if (DateTime.TryParse(value, out var dt)) return dt; return null; } private static string? MapStatus(string? dynamoStatus) { return dynamoStatus switch { "new" => "Open", "assigned" => "Open", "in_progress" => "In Progress", "on_hold" => "On Hold", "completed" => "Done", "cancelled" => "Cancelled", "unknown" => "Open", _ => "Open" }; } private static string? MapSeverityToPriority(string? severity) { if (string.IsNullOrWhiteSpace(severity)) return null; return $"Sev {severity}"; } } }