using System.Text.Json; using eSPJ.Models; namespace eSPJ.Services { public class FileDetailPenjemputanStore : IDetailPenjemputanStore { private readonly string _storeFilePath; private readonly ILogger _logger; private static readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1); public FileDetailPenjemputanStore( IWebHostEnvironment env, ILogger logger) { _logger = logger; _storeFilePath = Path.Combine(env.ContentRootPath, "Data", "detail-penjemputan.json"); } public async Task> GetAllAsync() { await _fileLock.WaitAsync(); try { return await ReadAllFromDiskAsync(); } finally { _fileLock.Release(); } } private async Task> ReadAllFromDiskAsync() { try { if (!File.Exists(_storeFilePath)) { return new List(); } var json = await File.ReadAllTextAsync(_storeFilePath); if (string.IsNullOrWhiteSpace(json)) { return new List(); } var data = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return data?.Where(item => item != null).ToList() ?? new List(); } catch (Exception ex) { _logger.LogError(ex, "Error reading submitted penjemputan data"); return new List(); } } public async Task> GetByNomorSpjAsync(string nomorSpj) { var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(normalizedNomorSpj)) { return new List(); } var allData = await GetAllAsync(); return allData .Where(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(item => item.UpdatedAt) .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) .Select(group => group.First()) .OrderBy(item => item.Name) .ToList(); } public async Task> GetSubmittedAsync() { var allData = await GetAllAsync(); return allData .Where(item => item.IsSubmit) .OrderByDescending(item => item.UpdatedAt) .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) .Select(group => group.First()) .ToList(); } private static string GetRecordIdentity(TpsData item) { return string.Join("::", new[] { item.NomorSpj?.Trim() ?? string.Empty, item.SpjDetailId?.Trim() ?? string.Empty, item.LokasiAngkutId?.Trim() ?? string.Empty, item.Name?.Trim() ?? string.Empty, }); } public async Task GetSubmittedDetailAsync(string nomorSpj, string? spjDetailId = null, string? lokasiAngkutId = null, string? namaTps = null) { var normalizedNomorSpj = (nomorSpj ?? string.Empty).Trim(); var normalizedSpjDetailId = (spjDetailId ?? string.Empty).Trim(); var normalizedLokasiAngkutId = (lokasiAngkutId ?? string.Empty).Trim(); var normalizedNamaTps = (namaTps ?? string.Empty).Trim(); var allData = await GetSubmittedAsync(); return allData.FirstOrDefault(item => string.Equals((item.NomorSpj ?? string.Empty).Trim(), normalizedNomorSpj, StringComparison.OrdinalIgnoreCase) && (string.IsNullOrWhiteSpace(normalizedSpjDetailId) || string.Equals((item.SpjDetailId ?? string.Empty).Trim(), normalizedSpjDetailId, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrWhiteSpace(normalizedLokasiAngkutId) || string.Equals((item.LokasiAngkutId ?? string.Empty).Trim(), normalizedLokasiAngkutId, StringComparison.OrdinalIgnoreCase)) && (string.IsNullOrWhiteSpace(normalizedNamaTps) || string.Equals((item.Name ?? string.Empty).Trim(), normalizedNamaTps, StringComparison.OrdinalIgnoreCase))); } public async Task SaveSubmittedAsync(TpsData data) { await _fileLock.WaitAsync(); try { var allData = await ReadAllFromDiskAsync(); data.IsSubmit = true; data.UpdatedAt = DateTime.Now; if (!data.SubmittedAt.HasValue) { data.SubmittedAt = DateTime.Now; } var existingIndex = FindRecordIndex(allData, data.NomorSpj, data.SpjDetailId, data.LokasiAngkutId, data.Name); if (existingIndex >= 0) { allData[existingIndex] = data; } else { allData.Add(data); } await WriteAllToDiskAsync(allData); } finally { _fileLock.Release(); } } public async Task SaveRecordAsync(RecordSaveRequest request) { await _fileLock.WaitAsync(); try { var allData = await ReadAllFromDiskAsync(); var mapped = MapRequestToRecord(request); mapped.UpdatedAt = DateTime.Now; var existingIndex = FindRecordIndex(allData, mapped.NomorSpj, mapped.SpjDetailId, mapped.LokasiAngkutId, mapped.Name); if (existingIndex >= 0) { var existing = allData[existingIndex]; mapped.NomorSpj = string.IsNullOrWhiteSpace(mapped.NomorSpj) ? existing.NomorSpj : mapped.NomorSpj; mapped.LokasiAngkutId = string.IsNullOrWhiteSpace(mapped.LokasiAngkutId) ? existing.LokasiAngkutId : mapped.LokasiAngkutId; mapped.SpjDetailId = string.IsNullOrWhiteSpace(mapped.SpjDetailId) ? existing.SpjDetailId : mapped.SpjDetailId; mapped.Name = string.IsNullOrWhiteSpace(mapped.Name) ? existing.Name : mapped.Name; mapped.Latitude = string.IsNullOrWhiteSpace(mapped.Latitude) ? existing.Latitude : mapped.Latitude; mapped.Longitude = string.IsNullOrWhiteSpace(mapped.Longitude) ? existing.Longitude : mapped.Longitude; mapped.AlamatJalan = string.IsNullOrWhiteSpace(mapped.AlamatJalan) ? existing.AlamatJalan : mapped.AlamatJalan; mapped.WaktuKedatangan = string.IsNullOrWhiteSpace(mapped.WaktuKedatangan) ? existing.WaktuKedatangan : mapped.WaktuKedatangan; mapped.FotoKedatangan = mapped.FotoKedatangan.Count > 0 ? mapped.FotoKedatangan : (existing.FotoKedatangan ?? new List()); mapped.FotoKedatanganUploaded = mapped.FotoKedatanganUploaded || existing.FotoKedatanganUploaded; mapped.Timbangan = MergeTimbangan(existing.Timbangan, mapped.Timbangan); mapped.TotalOrganik = mapped.TotalOrganik != 0 ? mapped.TotalOrganik : existing.TotalOrganik; mapped.TotalAnorganik = mapped.TotalAnorganik != 0 ? mapped.TotalAnorganik : existing.TotalAnorganik; mapped.TotalResidu = mapped.TotalResidu != 0 ? mapped.TotalResidu : existing.TotalResidu; mapped.TotalTimbangan = mapped.TotalTimbangan != 0 ? mapped.TotalTimbangan : existing.TotalTimbangan; mapped.FotoPetugas = mapped.FotoPetugas.Count > 0 ? mapped.FotoPetugas : (existing.FotoPetugas ?? new List()); mapped.FotoPetugasUploaded = mapped.FotoPetugasUploaded || existing.FotoPetugasUploaded; mapped.NamaPetugas = string.IsNullOrWhiteSpace(mapped.NamaPetugas) ? existing.NamaPetugas : mapped.NamaPetugas; mapped.IsSubmit = existing.IsSubmit || mapped.IsSubmit; mapped.SubmittedAt = existing.SubmittedAt; allData[existingIndex] = mapped; } else { allData.Add(mapped); } await WriteAllToDiskAsync(allData); } finally { _fileLock.Release(); } } private static List MergeTimbangan(List? existingItems, List? incomingItems) { var existing = existingItems ?? new List(); var incoming = incomingItems ?? new List(); if (incoming.Count == 0) { return existing; } var maxCount = Math.Max(existing.Count, incoming.Count); var merged = new List(maxCount); for (var index = 0; index < maxCount; index++) { var existingItem = index < existing.Count ? existing[index] : null; var incomingItem = index < incoming.Count ? incoming[index] : null; if (existingItem == null && incomingItem == null) { continue; } if (existingItem == null) { merged.Add(CloneTimbanganItem(incomingItem!)); continue; } if (incomingItem == null) { merged.Add(CloneTimbanganItem(existingItem)); continue; } merged.Add(new TimbanganItem { FotoFileName = string.IsNullOrWhiteSpace(incomingItem.FotoFileName) ? existingItem.FotoFileName : incomingItem.FotoFileName, Berat = incomingItem.Berat != null && incomingItem.Berat.Count > 0 ? new List(incomingItem.Berat) : new List(existingItem.Berat ?? new List()), LokasiAngkut = incomingItem.LokasiAngkut != null && incomingItem.LokasiAngkut.Count > 0 ? new List(incomingItem.LokasiAngkut) : new List(existingItem.LokasiAngkut ?? new List()), JenisSampah = incomingItem.JenisSampah != null && incomingItem.JenisSampah.Count > 0 ? new List(incomingItem.JenisSampah) : new List(existingItem.JenisSampah ?? new List()), IsUploaded = existingItem.IsUploaded || incomingItem.IsUploaded, WaktuUpload = incomingItem.WaktuUpload ?? existingItem.WaktuUpload, }); } return merged; } private static TimbanganItem CloneTimbanganItem(TimbanganItem source) { return new TimbanganItem { FotoFileName = source.FotoFileName, Berat = new List(source.Berat ?? new List()), LokasiAngkut = new List(source.LokasiAngkut ?? new List()), JenisSampah = new List(source.JenisSampah ?? new List()), IsUploaded = source.IsUploaded, WaktuUpload = source.WaktuUpload, }; } private async Task WriteAllToDiskAsync(List data) { var directory = Path.GetDirectoryName(_storeFilePath); if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } data = data .Where(item => item != null) .OrderByDescending(item => item.UpdatedAt) .GroupBy(GetRecordIdentity, StringComparer.OrdinalIgnoreCase) .Select(group => group.First()) .OrderBy(item => item.Name) .ToList(); var options = new JsonSerializerOptions { WriteIndented = true }; var json = JsonSerializer.Serialize(data, options); var tempPath = _storeFilePath + ".tmp"; await File.WriteAllTextAsync(tempPath, json); File.Move(tempPath, _storeFilePath, overwrite: true); } private static int FindRecordIndex(List allData, string? nomorSpj, string? spjDetailId, string? lokasiAngkutId, string? namaTps) { return allData.FindIndex(item => item != null && !string.IsNullOrWhiteSpace(nomorSpj) && string.Equals(item.NomorSpj, nomorSpj, StringComparison.OrdinalIgnoreCase) && string.Equals(item.SpjDetailId, spjDetailId ?? string.Empty, StringComparison.OrdinalIgnoreCase) && string.Equals(item.LokasiAngkutId, lokasiAngkutId ?? string.Empty, StringComparison.OrdinalIgnoreCase) && string.Equals(item.Name, namaTps ?? string.Empty, StringComparison.OrdinalIgnoreCase)); } private static TpsData MapRequestToRecord(RecordSaveRequest request) { return new TpsData { NomorSpj = request.NomorSpj, LokasiAngkutId = request.LokasiAngkutId, SpjDetailId = request.SpjDetailId, Name = request.NamaTps, Latitude = request.Latitude, Longitude = request.Longitude, AlamatJalan = request.AlamatJalan, WaktuKedatangan = request.WaktuKedatangan, FotoKedatangan = request.FotoKedatanganFileNames ?? new List(), FotoKedatanganUploaded = request.FotoKedatanganUploaded, Timbangan = (request.Timbangan ?? new List()) .Where(item => item != null) .Select(item => new TimbanganItem { FotoFileName = item.FotoFileName, Berat = new List { item.Berat }, LokasiAngkut = new List(), JenisSampah = Enum.TryParse(item.JenisSampah, out var parsedJenis) ? new List { parsedJenis } : new List { JenisSampah.Residu }, IsUploaded = item.Uploaded, WaktuUpload = DateTime.Now }).ToList(), TotalOrganik = request.TotalOrganik, TotalAnorganik = request.TotalAnorganik, TotalResidu = request.TotalResidu, TotalTimbangan = request.TotalTimbangan, FotoPetugas = request.FotoPetugasFileNames ?? new List(), FotoPetugasUploaded = request.FotoPetugasUploaded, NamaPetugas = request.NamaPetugas, IsSubmit = request.IsSubmit, UpdatedAt = DateTime.Now, SubmittedAt = request.IsSubmit ? DateTime.Now : null, }; } } }