mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
- Add InternalWONumber to WorkOrder model - Auto-generate 8-digit sequential numbers starting at 10000001 - Generate on new work order creation and DynamoDB sync - Include in GetWorkorderById and GetWorkOrderList responses - Add BackfillInternalWONumbers endpoint for existing records - Add migration for new column
324 lines
12 KiB
C#
324 lines
12 KiB
C#
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<IActionResult> SyncWorkOrders()
|
|
{
|
|
var synced = 0;
|
|
var created = 0;
|
|
var updated = 0;
|
|
var locationsCreated = 0;
|
|
|
|
var lastInternalWO = await _db.workOrders
|
|
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
|
|
.OrderByDescending(w => w.InternalWONumber)
|
|
.Select(w => w.InternalWONumber)
|
|
.FirstOrDefaultAsync();
|
|
int nextInternal = 10000001;
|
|
if (lastInternalWO != null && int.TryParse(lastInternalWO, out var parsed))
|
|
nextInternal = parsed + 1;
|
|
|
|
Dictionary<string, AttributeValue>? 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
|
|
{
|
|
InternalWONumber = (nextInternal++).ToString("D8"),
|
|
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<IActionResult> SyncComments()
|
|
{
|
|
var synced = 0;
|
|
var created = 0;
|
|
var skipped = 0;
|
|
Dictionary<string, AttributeValue>? 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"),
|
|
CommentType = "customer",
|
|
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("BackfillInternalWONumbers")]
|
|
public async Task<IActionResult> BackfillInternalWONumbers()
|
|
{
|
|
var wosMissing = await _db.workOrders
|
|
.Where(w => w.InternalWONumber == null || w.InternalWONumber == "")
|
|
.OrderBy(w => w.Id)
|
|
.ToListAsync();
|
|
|
|
if (wosMissing.Count == 0)
|
|
return Ok(new { updated = 0 });
|
|
|
|
var lastWO = await _db.workOrders
|
|
.Where(w => w.InternalWONumber != null && w.InternalWONumber != "")
|
|
.OrderByDescending(w => w.InternalWONumber)
|
|
.Select(w => w.InternalWONumber)
|
|
.FirstOrDefaultAsync();
|
|
|
|
int next = 10000001;
|
|
if (lastWO != null && int.TryParse(lastWO, out var lastNum))
|
|
next = lastNum + 1;
|
|
|
|
foreach (var wo in wosMissing)
|
|
{
|
|
wo.InternalWONumber = next.ToString("D8");
|
|
next++;
|
|
}
|
|
|
|
await _db.SaveChangesAsync();
|
|
return Ok(new { updated = wosMissing.Count });
|
|
}
|
|
|
|
[HttpPost("BackfillCommentTypes")]
|
|
public async Task<IActionResult> BackfillCommentTypes()
|
|
{
|
|
var updated = await _db.Database.ExecuteSqlRawAsync(
|
|
"UPDATE Comments SET CommentType = 'customer' WHERE ExternalCommentId IS NOT NULL AND (CommentType IS NULL OR CommentType = '')");
|
|
return Ok(new { updated });
|
|
}
|
|
|
|
[HttpPost("All")]
|
|
public async Task<IActionResult> 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<int?> 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<string, AttributeValue> 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}";
|
|
}
|
|
}
|
|
}
|