update: driver upst timestamp dll
parent
a6584554f2
commit
f9b74caedc
|
|
@ -3,6 +3,8 @@ using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using eSPJ.Models;
|
||||||
|
using eSPJ.Services;
|
||||||
|
|
||||||
namespace eSPJ.Controllers.SpjDriverUpstController
|
namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
{
|
{
|
||||||
|
|
@ -11,11 +13,19 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly DetailPenjemputanService _detailService;
|
||||||
|
private readonly ILogger<DetailPenjemputanController> _logger;
|
||||||
|
|
||||||
public DetailPenjemputanController(IHttpClientFactory httpClientFactory, IConfiguration configuration)
|
public DetailPenjemputanController(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
DetailPenjemputanService detailService,
|
||||||
|
ILogger<DetailPenjemputanController> logger)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_detailService = detailService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
|
@ -28,61 +38,38 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
{
|
{
|
||||||
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/TanpaTps.cshtml");
|
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/TanpaTps.cshtml");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("batal")]
|
||||||
|
public IActionResult Batal()
|
||||||
|
{
|
||||||
|
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml");
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public IActionResult Index(
|
public async Task<IActionResult> Submit([FromForm] DetailPenjemputanRequest request)
|
||||||
string? Latitude,
|
|
||||||
string? Longitude,
|
|
||||||
string? AlamatJalan,
|
|
||||||
string? GpsTruck,
|
|
||||||
string? WaktuKedatangan,
|
|
||||||
decimal? TotalTimbangan,
|
|
||||||
List<decimal>? BeratTimbangan,
|
|
||||||
List<IFormFile>? FotoKedatangan,
|
|
||||||
List<IFormFile>? FotoTimbangan,
|
|
||||||
List<IFormFile>? FotoPetugas)
|
|
||||||
{
|
{
|
||||||
if (FotoKedatangan == null || FotoKedatangan.Count == 0)
|
try
|
||||||
{
|
{
|
||||||
TempData["Error"] = "Step 1 wajib upload minimal 1 foto kedatangan.";
|
var result = await _detailService.SubmitPenjemputanAsync(request);
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = result.Message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = result.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FotoTimbangan == null || FotoTimbangan.Count == 0)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Step 2 wajib upload minimal 1 foto timbangan.";
|
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
if (FotoPetugas == null || FotoPetugas.Count == 0)
|
|
||||||
{
|
{
|
||||||
TempData["Error"] = "Step 3 wajib upload minimal 1 foto petugas.";
|
_logger.LogError(ex, "Error submitting penjemputan data");
|
||||||
|
TempData["Error"] = "Terjadi kesalahan saat menyimpan data.";
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalByDetail = (BeratTimbangan ?? new List<decimal>())
|
|
||||||
.Where(x => x > 0)
|
|
||||||
.Sum();
|
|
||||||
|
|
||||||
var total = TotalTimbangan.GetValueOrDefault() > 0
|
|
||||||
? TotalTimbangan.GetValueOrDefault()
|
|
||||||
: totalByDetail;
|
|
||||||
|
|
||||||
var totalDisplay = total.ToString("N2", CultureInfo.GetCultureInfo("id-ID"));
|
|
||||||
|
|
||||||
TempData["Success"] =
|
|
||||||
$"Data tersimpan. Kedatangan: {FotoKedatangan.Count} foto, " +
|
|
||||||
$"Timbangan: {FotoTimbangan.Count} foto, Total: {totalDisplay} kg, " +
|
|
||||||
$"Petugas: {FotoPetugas.Count} foto.";
|
|
||||||
|
|
||||||
// TODO: simpan ke database
|
|
||||||
_ = Latitude;
|
|
||||||
_ = Longitude;
|
|
||||||
_ = AlamatJalan;
|
|
||||||
_ = GpsTruck;
|
|
||||||
_ = WaktuKedatangan;
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("ocr-timbangan")]
|
[HttpPost("ocr-timbangan")]
|
||||||
|
|
@ -94,7 +81,6 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
return BadRequest(new { success = false, message = "Foto tidak ditemukan." });
|
return BadRequest(new { success = false, message = "Foto tidak ditemukan." });
|
||||||
}
|
}
|
||||||
|
|
||||||
// limit size biar ga gila (contoh 5MB)
|
|
||||||
if (Foto.Length > 5 * 1024 * 1024)
|
if (Foto.Length > 5 * 1024 * 1024)
|
||||||
{
|
{
|
||||||
return BadRequest(new { success = false, message = "Ukuran foto terlalu besar. Maksimal 5MB." });
|
return BadRequest(new { success = false, message = "Ukuran foto terlalu besar. Maksimal 5MB." });
|
||||||
|
|
@ -119,7 +105,10 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
|
|
||||||
var payload = new
|
var payload = new
|
||||||
{
|
{
|
||||||
model = "nvidia/nemotron-nano-12b-v2-vl:free",
|
// model = "nvidia/nemotron-nano-12b-v2-vl:free",
|
||||||
|
model = "google/gemini-2.5-flash-image",
|
||||||
|
// model = "google/gemini-2.5-flash-lite",
|
||||||
|
// model = "google/gemini-2.5-flash-lite-preview-09-2025",
|
||||||
temperature = 0,
|
temperature = 0,
|
||||||
messages = new object[]
|
messages = new object[]
|
||||||
{
|
{
|
||||||
|
|
@ -253,10 +242,5 @@ namespace eSPJ.Controllers.SpjDriverUpstController
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("batal")]
|
|
||||||
public IActionResult Batal()
|
|
||||||
{
|
|
||||||
return View("~/Views/Admin/Transport/SpjDriverUpst/DetailPenjemputan/Batal.cshtml");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
namespace eSPJ.Models
|
||||||
|
{
|
||||||
|
public enum JenisSampah
|
||||||
|
{
|
||||||
|
Organik,
|
||||||
|
Anorganik,
|
||||||
|
Residu
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TimbanganItem
|
||||||
|
{
|
||||||
|
public string? FotoFileName { get; set; }
|
||||||
|
public decimal Berat { get; set; }
|
||||||
|
public JenisSampah JenisSampah { get; set; } = JenisSampah.Residu;
|
||||||
|
public bool IsUploaded { get; set; }
|
||||||
|
public DateTime? WaktuUpload { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TpsData
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int Index { get; set; }
|
||||||
|
public string Latitude { get; set; } = string.Empty;
|
||||||
|
public string Longitude { get; set; } = string.Empty;
|
||||||
|
public string AlamatJalan { get; set; } = string.Empty;
|
||||||
|
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||||
|
public List<string> FotoKedatangan { get; set; } = new();
|
||||||
|
public bool FotoKedatanganUploaded { get; set; }
|
||||||
|
public List<TimbanganItem> Timbangan { get; set; } = new();
|
||||||
|
public decimal TotalOrganik { get; set; }
|
||||||
|
public decimal TotalAnorganik { get; set; }
|
||||||
|
public decimal TotalResidu { get; set; }
|
||||||
|
public decimal TotalTimbangan { get; set; }
|
||||||
|
public List<string> FotoPetugas { get; set; } = new();
|
||||||
|
public bool FotoPetugasUploaded { get; set; }
|
||||||
|
public string NamaPetugas { get; set; } = string.Empty;
|
||||||
|
public bool Submitted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DetailPenjemputanRequest
|
||||||
|
{
|
||||||
|
public string TpsName { get; set; } = string.Empty;
|
||||||
|
public string Latitude { get; set; } = string.Empty;
|
||||||
|
public string Longitude { get; set; } = string.Empty;
|
||||||
|
public string AlamatJalan { get; set; } = string.Empty;
|
||||||
|
public string WaktuKedatangan { get; set; } = string.Empty;
|
||||||
|
public decimal TotalTimbangan { get; set; }
|
||||||
|
public decimal TotalOrganik { get; set; }
|
||||||
|
public decimal TotalAnorganik { get; set; }
|
||||||
|
public decimal TotalResidu { get; set; }
|
||||||
|
public string NamaPetugas { get; set; } = string.Empty;
|
||||||
|
public List<IFormFile>? FotoKedatangan { get; set; }
|
||||||
|
public List<IFormFile>? FotoTimbangan { get; set; }
|
||||||
|
public List<decimal>? BeratTimbangan { get; set; }
|
||||||
|
public List<string>? JenisSampahList { get; set; }
|
||||||
|
public List<IFormFile>? FotoPetugas { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public object? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OcrTimbanganRequest
|
||||||
|
{
|
||||||
|
public IFormFile? Foto { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OcrTimbanganResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Weight { get; set; }
|
||||||
|
public string? Raw { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
using eSPJ.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Register custom services
|
||||||
|
builder.Services.AddScoped<DetailPenjemputanService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
@ -26,5 +31,4 @@ app.MapControllerRoute(
|
||||||
pattern: "{controller=Home}/{action=Index}/{id?}")
|
pattern: "{controller=Home}/{action=Index}/{id?}")
|
||||||
.WithStaticAssets();
|
.WithStaticAssets();
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using eSPJ.Models;
|
||||||
|
|
||||||
|
namespace eSPJ.Services
|
||||||
|
{
|
||||||
|
public class DetailPenjemputanService
|
||||||
|
{
|
||||||
|
private readonly string _dataFilePath;
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
private readonly ILogger<DetailPenjemputanService> _logger;
|
||||||
|
|
||||||
|
public DetailPenjemputanService(
|
||||||
|
IWebHostEnvironment env,
|
||||||
|
ILogger<DetailPenjemputanService> logger)
|
||||||
|
{
|
||||||
|
_env = env;
|
||||||
|
_logger = logger;
|
||||||
|
_dataFilePath = Path.Combine(_env.ContentRootPath, "Data", "detail-penjemputan.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TpsData>> GetAllTpsDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_dataFilePath))
|
||||||
|
{
|
||||||
|
return new List<TpsData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(_dataFilePath);
|
||||||
|
var data = JsonSerializer.Deserialize<List<TpsData>>(json);
|
||||||
|
return data ?? new List<TpsData>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error reading TPS data from JSON");
|
||||||
|
return new List<TpsData>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveTpsDataAsync(List<TpsData> data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(_dataFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(data, options);
|
||||||
|
await File.WriteAllTextAsync(_dataFilePath, json);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving TPS data to JSON");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DetailPenjemputanResponse> SubmitPenjemputanAsync(DetailPenjemputanRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate request
|
||||||
|
if (string.IsNullOrEmpty(request.TpsName))
|
||||||
|
{
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Nama TPS harus diisi"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.FotoKedatangan == null || !request.FotoKedatangan.Any())
|
||||||
|
{
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Foto kedatangan harus diupload"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.FotoTimbangan == null || !request.FotoTimbangan.Any())
|
||||||
|
{
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Foto timbangan harus diupload"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.FotoPetugas == null || !request.FotoPetugas.Any())
|
||||||
|
{
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Foto petugas harus diupload"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.NamaPetugas))
|
||||||
|
{
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Nama petugas harus diisi"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save files
|
||||||
|
var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "penjemputan", DateTime.Now.ToString("yyyy-MM-dd"));
|
||||||
|
if (!Directory.Exists(uploadPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpsData = new TpsData
|
||||||
|
{
|
||||||
|
Name = request.TpsName,
|
||||||
|
Latitude = request.Latitude,
|
||||||
|
Longitude = request.Longitude,
|
||||||
|
AlamatJalan = request.AlamatJalan,
|
||||||
|
WaktuKedatangan = request.WaktuKedatangan,
|
||||||
|
TotalTimbangan = request.TotalTimbangan,
|
||||||
|
TotalOrganik = request.TotalOrganik,
|
||||||
|
TotalAnorganik = request.TotalAnorganik,
|
||||||
|
TotalResidu = request.TotalResidu,
|
||||||
|
NamaPetugas = request.NamaPetugas,
|
||||||
|
Submitted = true,
|
||||||
|
FotoKedatanganUploaded = true,
|
||||||
|
FotoPetugasUploaded = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save foto kedatangan
|
||||||
|
foreach (var file in request.FotoKedatangan)
|
||||||
|
{
|
||||||
|
var fileName = $"kedatangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||||
|
var filePath = Path.Combine(uploadPath, fileName);
|
||||||
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
tpsData.FotoKedatangan.Add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save foto timbangan
|
||||||
|
if (request.BeratTimbangan != null && request.JenisSampahList != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < request.FotoTimbangan.Count; i++)
|
||||||
|
{
|
||||||
|
var file = request.FotoTimbangan[i];
|
||||||
|
var fileName = $"timbangan_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||||
|
var filePath = Path.Combine(uploadPath, fileName);
|
||||||
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jenisSampah = JenisSampah.Residu;
|
||||||
|
if (i < request.JenisSampahList.Count && Enum.TryParse<JenisSampah>(request.JenisSampahList[i], out var parsed))
|
||||||
|
{
|
||||||
|
jenisSampah = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
tpsData.Timbangan.Add(new TimbanganItem
|
||||||
|
{
|
||||||
|
FotoFileName = fileName,
|
||||||
|
Berat = i < request.BeratTimbangan.Count ? request.BeratTimbangan[i] : 0,
|
||||||
|
JenisSampah = jenisSampah,
|
||||||
|
IsUploaded = true,
|
||||||
|
WaktuUpload = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save foto petugas
|
||||||
|
foreach (var file in request.FotoPetugas)
|
||||||
|
{
|
||||||
|
var fileName = $"petugas_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||||
|
var filePath = Path.Combine(uploadPath, fileName);
|
||||||
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
tpsData.FotoPetugas.Add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing data and append
|
||||||
|
var allData = await GetAllTpsDataAsync();
|
||||||
|
allData.Add(tpsData);
|
||||||
|
await SaveTpsDataAsync(allData);
|
||||||
|
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Data penjemputan berhasil disimpan",
|
||||||
|
Data = tpsData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error submitting penjemputan data");
|
||||||
|
return new DetailPenjemputanResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Terjadi kesalahan: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OcrTimbanganResponse> ProcessOcrTimbanganAsync(IFormFile foto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// TODO: Integrate with OpenRouter API
|
||||||
|
// For now, return mock response
|
||||||
|
await Task.Delay(500); // Simulate API call
|
||||||
|
|
||||||
|
return new OcrTimbanganResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Weight = "54.50",
|
||||||
|
Raw = "54.50 kg",
|
||||||
|
Message = "OCR processed successfully (mock)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing OCR timbangan");
|
||||||
|
return new OcrTimbanganResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Terjadi kesalahan: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,21 +4,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen">
|
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen">
|
||||||
<div class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-4 py-4 sticky top-0 z-10 shadow-lg">
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="bg-upst text-white px-6 pt-8 pb-16 rounded-b-[40px] shadow-lg relative">
|
||||||
<a href="@Url.Action("Index", "Home")" class="p-2 hover:bg-white/10 rounded-full transition-all duration-200">
|
<div class="flex items-center justify-between relative z-10">
|
||||||
|
<a href="@Url.Action("Index", "Home")" class="w-10 h-10 flex items-center justify-center bg-white/10 rounded-xl backdrop-blur-md">
|
||||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="text-lg font-bold">Pembatalan Penjemputan</h1>
|
<h1 class="text-lg font-black uppercase tracking-tight">Batal Angkut</h1>
|
||||||
<div class="w-9"></div>
|
<div class="w-10"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 pt-4">
|
<div class="px-4 pt-4">
|
||||||
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 mb-4">
|
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 mb-4">
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="flex items-center gap-3 mb-3">
|
||||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<i class="w-5 h-5 text-orange-600" data-lucide="map-pin"></i>
|
<i class="w-5 h-5 text-gray-600" data-lucide="map-pin"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-gray-900">CV Tri Berkah Sejahtera</h3>
|
<h3 class="font-bold text-gray-900">CV Tri Berkah Sejahtera</h3>
|
||||||
|
|
@ -34,8 +35,8 @@
|
||||||
<div class="px-4 pb-6">
|
<div class="px-4 pb-6">
|
||||||
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
|
<div class="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<i class="w-5 h-5 text-orange-600" data-lucide="file-text"></i>
|
<i class="w-5 h-5 text-gray-600" data-lucide="file-text"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-bold text-gray-900">Form Pembatalan</h2>
|
<h2 class="text-lg font-bold text-gray-900">Form Pembatalan</h2>
|
||||||
|
|
@ -57,13 +58,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Foto Bukti -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-3">Foto Bukti Pembatalan</label>
|
||||||
|
<input type="file"
|
||||||
|
name="FotoBukti"
|
||||||
|
id="fotoBukti"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
class="block w-full text-sm text-gray-700 border border-gray-300 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-red-500 file:px-4 file:py-2 file:text-xs file:font-bold file:text-white hover:file:bg-red-600 transition-all duration-200" />
|
||||||
|
<p class="text-xs text-gray-500 mt-2">Upload foto sebagai bukti pembatalan
|
||||||
|
<div id="preview-container" class="mt-3 space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<a href="@Url.Action("Index", "Home")"
|
<a href="@Url.Action("Index", "Home")"
|
||||||
class="flex-1 bg-gray-200 text-gray-700 font-semibold py-3 rounded-xl text-center hover:bg-gray-300 transition-colors">
|
class="flex-1 bg-gray-200 text-gray-700 font-semibold py-3 rounded-xl text-center hover:bg-gray-300 transition-colors">
|
||||||
Batal
|
Batal
|
||||||
</a>
|
</a>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="flex-1 bg-gradient-to-r from-orange-500 to-orange-600 text-white font-semibold py-3 rounded-xl hover:from-orange-600 hover:to-orange-700 transition-all duration-200 shadow-lg">
|
class="flex-1 bg-upst text-white font-semibold py-3 rounded-xl hover:from-gray-600 hover:to-gray-700 transition-all duration-200 shadow-lg">
|
||||||
Konfirmasi
|
Konfirmasi
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,6 +85,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Bottom Navigation -->
|
<!-- Bottom Navigation -->
|
||||||
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,6 +96,186 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const alasanTextarea = document.querySelector('textarea[name="AlasanPembatalan"]');
|
const alasanTextarea = document.querySelector('textarea[name="AlasanPembatalan"]');
|
||||||
const form = document.querySelector('form');
|
const form = document.querySelector('form');
|
||||||
const validationMessage = document.getElementById('validation-message');
|
const validationMessage = document.getElementById('validation-message');
|
||||||
|
const fotoBuktiInput = document.getElementById('fotoBukti');
|
||||||
|
const previewContainer = document.getElementById('preview-container');
|
||||||
|
|
||||||
|
// Fungsi watermark untuk foto pembatalan
|
||||||
|
async function applyWatermark(file, photoNumber) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = function() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
|
||||||
|
|
||||||
|
const dayName = days[now.getDay()];
|
||||||
|
const date = now.getDate().toString().padStart(2, '0');
|
||||||
|
const month = months[now.getMonth()];
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const hours = now.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||||
|
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const timestamp = `${dayName}, ${date} ${month} ${year} • ${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
const baseFontSize = Math.max(16, Math.min(img.width, img.height) * 0.025);
|
||||||
|
const fontFamily = "'Montserrat', 'Segoe UI', 'Roboto', sans-serif";
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
{ text: `FOTO PEMBATALAN #${photoNumber}`, size: baseFontSize * 1.1, weight: '900', color: '#EF4444' },
|
||||||
|
{ text: 'SPJ/07-2025/PKM/000476', size: baseFontSize * 0.9, weight: '700', color: '#FFFFFF' },
|
||||||
|
{ text: timestamp, size: baseFontSize * 0.75, weight: '500', color: '#E2E8F0' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const paddingX = baseFontSize * 1.2;
|
||||||
|
const paddingY = baseFontSize * 1.0;
|
||||||
|
const lineGap = baseFontSize * 0.4;
|
||||||
|
|
||||||
|
let maxWidth = 0;
|
||||||
|
let totalHeight = 0;
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
||||||
|
const metrics = ctx.measureText(line.text);
|
||||||
|
maxWidth = Math.max(maxWidth, metrics.width);
|
||||||
|
totalHeight += line.size + lineGap;
|
||||||
|
});
|
||||||
|
totalHeight -= lineGap;
|
||||||
|
|
||||||
|
const margin = baseFontSize * 1.5;
|
||||||
|
const boxWidth = maxWidth + (paddingX * 2);
|
||||||
|
const boxHeight = totalHeight + (paddingY * 2);
|
||||||
|
const boxX = img.width - boxWidth - margin;
|
||||||
|
const boxY = img.height - boxHeight - margin;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
if (ctx.roundRect) {
|
||||||
|
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, baseFontSize * 0.8);
|
||||||
|
} else {
|
||||||
|
ctx.rect(boxX, boxY, boxWidth, boxHeight);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'rgba(15, 23, 42, 0.85)';
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Garis aksen merah untuk pembatalan
|
||||||
|
ctx.beginPath();
|
||||||
|
const accentWidth = baseFontSize * 0.3;
|
||||||
|
if (ctx.roundRect) {
|
||||||
|
ctx.roundRect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight, [0, baseFontSize * 0.8, baseFontSize * 0.8, 0]);
|
||||||
|
} else {
|
||||||
|
ctx.rect(boxX + boxWidth - accentWidth, boxY, accentWidth, boxHeight);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = '#EF4444';
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.6)';
|
||||||
|
ctx.shadowBlur = 4;
|
||||||
|
ctx.shadowOffsetX = 1;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
let currentY = boxY + paddingY;
|
||||||
|
const textRightLimit = boxX + boxWidth - paddingX - accentWidth;
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
ctx.font = `${line.weight} ${line.size}px ${fontFamily}`;
|
||||||
|
ctx.fillStyle = line.color;
|
||||||
|
ctx.fillText(line.text, textRightLimit, currentY);
|
||||||
|
currentY += line.size + lineGap;
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.shadowColor = 'transparent';
|
||||||
|
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
const watermarkedFile = new File([blob], file.name, {
|
||||||
|
type: 'image/jpeg',
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
resolve(watermarkedFile);
|
||||||
|
}, 'image/jpeg', 0.95);
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview foto dengan watermark
|
||||||
|
fotoBuktiInput.addEventListener('change', async function() {
|
||||||
|
previewContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (this.files.length > 5) {
|
||||||
|
alert('Maksimal 5 foto yang dapat diupload');
|
||||||
|
this.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan loading
|
||||||
|
previewContainer.innerHTML = '<div class="text-center py-4"><div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-red-500"></div><p class="text-sm text-gray-600 mt-2">Memproses foto...</p></div>';
|
||||||
|
|
||||||
|
const watermarkedFiles = [];
|
||||||
|
for (let i = 0; i < this.files.length; i++) {
|
||||||
|
const file = this.files[i];
|
||||||
|
try {
|
||||||
|
const watermarkedFile = await applyWatermark(file, i + 1);
|
||||||
|
watermarkedFiles.push(watermarkedFile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying watermark:', error);
|
||||||
|
watermarkedFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus loading dan tampilkan preview
|
||||||
|
previewContainer.innerHTML = '';
|
||||||
|
|
||||||
|
watermarkedFiles.forEach((file, index) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const previewItem = document.createElement('div');
|
||||||
|
previewItem.className = 'rounded-xl border border-red-200 overflow-hidden bg-black shadow-sm hover:shadow-md transition-shadow';
|
||||||
|
previewItem.innerHTML = `
|
||||||
|
<div class="h-40 bg-black/80">
|
||||||
|
<img src="${e.target.result}" alt="Preview ${index + 1}" class="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-2 bg-white">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<div class="flex-shrink-0 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-white text-xs font-bold">${index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs font-semibold text-gray-700 truncate flex-1">${file.name}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-[10px] text-gray-500">
|
||||||
|
<span>${(file.size / (1024 * 1024)).toFixed(2)} MB</span>
|
||||||
|
<span class="text-red-600 font-semibold">✓ Watermark</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
previewContainer.appendChild(previewItem);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update file input dengan watermarked files
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
watermarkedFiles.forEach(file => dataTransfer.items.add(file));
|
||||||
|
this.files = dataTransfer.files;
|
||||||
|
});
|
||||||
|
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
if (!alasanTextarea.value.trim()) {
|
if (!alasanTextarea.value.trim()) {
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4 border-t border-gray-100 mt-4">
|
<div class="pt-4 border-t border-gray-100 mt-4 space-y-3">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<p class="text-[10px] text-gray-400 uppercase tracking-widest mb-2">Total Berat Semua TPS</p>
|
||||||
<p class="text-[10px] text-gray-400 uppercase tracking-widest">Total Berat Semua TPS</p>
|
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div class="bg-green-50 rounded-xl p-2">
|
||||||
|
<p class="text-[9px] text-green-600 font-bold uppercase tracking-wider">Organik</p>
|
||||||
|
<p class="text-sm font-black text-green-700"><span id="grand-total-organik">0,00</span> kg</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-50 rounded-xl p-2">
|
||||||
|
<p class="text-[9px] text-blue-600 font-bold uppercase tracking-wider">Anorganik</p>
|
||||||
|
<p class="text-sm font-black text-blue-700"><span id="grand-total-anorganik">0,00</span> kg</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-50 rounded-xl p-2">
|
||||||
|
<p class="text-[9px] text-red-600 font-bold uppercase tracking-wider">Residu</p>
|
||||||
|
<p class="text-sm font-black text-red-700"><span id="grand-total-residu">0,00</span> kg</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||||
|
<p class="text-xs font-bold text-gray-600">Total Keseluruhan</p>
|
||||||
<span class="text-2xl font-black text-upst"><span id="grand-total-timbangan">0,00</span> kg</span>
|
<span class="text-2xl font-black text-upst"><span id="grand-total-timbangan">0,00</span> kg</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,914 +122,10 @@
|
||||||
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<register-block dynamic-section="scripts" key="jsDetailPenjemputan">
|
<!-- <register-block dynamic-section="scripts" key="jsDetailPenjemputan">
|
||||||
<script>
|
<script src="~/driver/js/detail-penjemputan.js"></script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
</register-block> -->
|
||||||
const grandTotalDisplay = document.getElementById('grand-total-timbangan');
|
|
||||||
const tpsSelectionContainer = document.getElementById('tps-selection-container');
|
|
||||||
const tpsTabsContainer = document.getElementById('tps-tabs-container');
|
|
||||||
const tpsCheckboxesContainer = document.getElementById('tps-checkboxes');
|
|
||||||
const btnConfirmTps = document.getElementById('btn-confirm-tps');
|
|
||||||
const tpsTabsEl = document.getElementById('tps-tabs');
|
|
||||||
const tpsContentContainer = document.getElementById('tps-content');
|
|
||||||
|
|
||||||
let activeTpsIndex = 0;
|
@section Scripts {
|
||||||
let tpsData = [];
|
<script src="~/driver/js/detail-penjemputan.js"></script>
|
||||||
let availableTpsList = [];
|
}
|
||||||
let selectedTpsList = [];
|
|
||||||
let hasRequestedLocation = [];
|
|
||||||
|
|
||||||
const OCR_AREAS = [
|
|
||||||
{ id: 'A', x: 0.34, y: 0.35, w: 0.40, h: 0.11, color: 'border-lime-400 bg-lime-500/15' },
|
|
||||||
{ id: 'B', x: 0.31, y: 0.33, w: 0.45, h: 0.14, color: 'border-amber-300 bg-amber-400/10' },
|
|
||||||
{ id: 'C', x: 0.29, y: 0.31, w: 0.49, h: 0.17, color: 'border-cyan-300 bg-cyan-400/10' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function initializeLocation(tpsList) {
|
|
||||||
availableTpsList = tpsList || [];
|
|
||||||
|
|
||||||
if (availableTpsList.length === 0) {
|
|
||||||
selectedTpsList = ['Lokasi Utama'];
|
|
||||||
initializeTpsData(selectedTpsList);
|
|
||||||
tpsTabsContainer.style.display = 'block';
|
|
||||||
renderSingleForm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (availableTpsList.length === 1) {
|
|
||||||
selectedTpsList = [availableTpsList[0]];
|
|
||||||
initializeTpsData(selectedTpsList);
|
|
||||||
tpsTabsContainer.style.display = 'block';
|
|
||||||
renderTabs();
|
|
||||||
renderTpsForm();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTpsSelection();
|
|
||||||
tpsSelectionContainer.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTpsSelection() {
|
|
||||||
tpsCheckboxesContainer.innerHTML = '';
|
|
||||||
availableTpsList.forEach((tpsName, index) => {
|
|
||||||
const wrapper = document.createElement('label');
|
|
||||||
wrapper.className = 'flex items-center gap-3 p-3 rounded-xl border border-gray-200 hover:bg-gray-50 cursor-pointer transition';
|
|
||||||
|
|
||||||
const checkbox = document.createElement('input');
|
|
||||||
checkbox.type = 'checkbox';
|
|
||||||
checkbox.value = tpsName;
|
|
||||||
checkbox.className = 'w-5 h-5 rounded border-gray-300 text-upst focus:ring-upst';
|
|
||||||
checkbox.checked = true;
|
|
||||||
|
|
||||||
const label = document.createElement('span');
|
|
||||||
label.className = 'text-sm font-bold text-gray-700';
|
|
||||||
label.textContent = tpsName;
|
|
||||||
|
|
||||||
wrapper.appendChild(checkbox);
|
|
||||||
wrapper.appendChild(label);
|
|
||||||
tpsCheckboxesContainer.appendChild(wrapper);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
btnConfirmTps.addEventListener('click', function() {
|
|
||||||
const checkboxes = tpsCheckboxesContainer.querySelectorAll('input[type="checkbox"]:checked');
|
|
||||||
selectedTpsList = Array.from(checkboxes).map(cb => cb.value);
|
|
||||||
|
|
||||||
if (selectedTpsList.length === 0) {
|
|
||||||
alert('Pilih minimal 1 TPS untuk diangkut!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeTpsData(selectedTpsList);
|
|
||||||
tpsSelectionContainer.style.display = 'none';
|
|
||||||
tpsTabsContainer.style.display = 'block';
|
|
||||||
|
|
||||||
if (selectedTpsList.length === 1) {
|
|
||||||
renderSingleForm();
|
|
||||||
} else {
|
|
||||||
renderTabs();
|
|
||||||
renderTpsForm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function initializeTpsData(tpsNames) {
|
|
||||||
tpsData = tpsNames.map((name, index) => ({
|
|
||||||
name: name,
|
|
||||||
index: index,
|
|
||||||
latitude: '',
|
|
||||||
longitude: '',
|
|
||||||
alamatJalan: '',
|
|
||||||
waktuKedatangan: '',
|
|
||||||
fotoKedatangan: [],
|
|
||||||
fotoKedatanganUploaded: false,
|
|
||||||
timbangan: [],
|
|
||||||
totalTimbangan: 0,
|
|
||||||
fotoPetugas: [],
|
|
||||||
fotoPetugasUploaded: false,
|
|
||||||
namaPetugas: '',
|
|
||||||
submitted: false
|
|
||||||
}));
|
|
||||||
hasRequestedLocation = new Array(tpsNames.length).fill(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSingleForm() {
|
|
||||||
tpsTabsEl.style.display = 'none';
|
|
||||||
activeTpsIndex = 0;
|
|
||||||
renderTpsForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTabs() {
|
|
||||||
tpsTabsEl.style.display = 'flex';
|
|
||||||
tpsTabsEl.innerHTML = '';
|
|
||||||
tpsData.forEach((tps, index) => {
|
|
||||||
const tab = document.createElement('button');
|
|
||||||
tab.type = 'button';
|
|
||||||
tab.className = `px-4 py-2 rounded-xl font-bold text-sm whitespace-nowrap transition ${
|
|
||||||
index === activeTpsIndex
|
|
||||||
? 'bg-upst text-white'
|
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
||||||
}`;
|
|
||||||
tab.textContent = tps.name;
|
|
||||||
if (tps.submitted) {
|
|
||||||
tab.innerHTML += ' <span class="text-xs">✓</span>';
|
|
||||||
}
|
|
||||||
tab.addEventListener('click', () => switchToTps(index));
|
|
||||||
tpsTabsEl.appendChild(tab);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchToTps(index) {
|
|
||||||
activeTpsIndex = index;
|
|
||||||
renderTabs();
|
|
||||||
renderTpsForm();
|
|
||||||
|
|
||||||
if (!hasRequestedLocation[index]) {
|
|
||||||
hasRequestedLocation[index] = true;
|
|
||||||
getLocationUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGrandTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTpsForm() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const showTpsName = selectedTpsList.length > 1 || availableTpsList.length > 0;
|
|
||||||
|
|
||||||
tpsContentContainer.innerHTML = `
|
|
||||||
<form class="space-y-5 pb-8" data-tps-index="${tps.index}">
|
|
||||||
<input type="hidden" class="tps-latitude" value="${tps.latitude}" />
|
|
||||||
<input type="hidden" class="tps-longitude" value="${tps.longitude}" />
|
|
||||||
<input type="hidden" class="tps-alamat-jalan" value="${tps.alamatJalan}" />
|
|
||||||
<input type="hidden" class="tps-total-timbangan" value="${tps.totalTimbangan}" />
|
|
||||||
|
|
||||||
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">1</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-black text-gray-800">Foto Kedatangan${showTpsName ? ' - ' + tps.name : ''}</h3>
|
|
||||||
<p class="text-xs text-gray-500">Upload foto kedatangan</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="block text-xs font-semibold text-gray-600">Upload Foto Kedatangan</label>
|
|
||||||
<input type="file" class="tps-foto-kedatangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
|
|
||||||
<div class="tps-preview-kedatangan space-y-2"></div>
|
|
||||||
|
|
||||||
${tps.fotoKedatangan.length > 0 && !tps.fotoKedatanganUploaded ? `
|
|
||||||
<button type="button" class="tps-btn-upload-kedatangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
|
|
||||||
Upload ${tps.fotoKedatangan.length} Foto Kedatangan
|
|
||||||
</button>
|
|
||||||
` : tps.fotoKedatanganUploaded ? `
|
|
||||||
<div class="text-center text-xs text-green-600 font-bold py-2">
|
|
||||||
✓ Foto kedatangan sudah diupload
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Latitude</label>
|
|
||||||
<input type="text" class="tps-display-latitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.latitude}" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Longitude</label>
|
|
||||||
<input type="text" class="tps-display-longitude w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.longitude}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Waktu Kedatangan</label>
|
|
||||||
<input type="text" class="tps-waktu-kedatangan w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-xs" readonly value="${tps.waktuKedatangan}" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">2</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-black text-gray-800">Foto Timbang Sampah</h3>
|
|
||||||
<p class="text-xs text-gray-500">Upload foto timbangan, berat auto terisi</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tps-timbangan-repeater space-y-3"></div>
|
|
||||||
|
|
||||||
<button type="button" class="tps-btn-add-timbangan w-full border border-dashed border-upst text-upst rounded-xl py-2 text-xs font-bold transition">
|
|
||||||
+ Tambah Foto Timbangan
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="rounded-xl bg-gray-50 border border-gray-200 px-3 py-2 flex items-center justify-between">
|
|
||||||
<span class="text-xs font-semibold text-gray-600">Total Timbangan${showTpsName ? ' ' + tps.name : ''}</span>
|
|
||||||
<span class="text-base font-black text-upst"><span class="tps-display-total">${formatWeightDisplay(tps.totalTimbangan)}</span> kg</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="bg-white border border-gray-100 rounded-3xl p-5 space-y-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-upst text-white font-black text-sm flex items-center justify-center">3</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-black text-gray-800">Foto Petugas</h3>
|
|
||||||
<p class="text-xs text-gray-500">Upload dokumentasi petugas</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="block text-xs font-semibold text-gray-600">Upload Foto Petugas</label>
|
|
||||||
<input type="file" class="tps-foto-petugas block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" accept="image/*" multiple />
|
|
||||||
<div class="tps-preview-petugas space-y-2"></div>
|
|
||||||
|
|
||||||
${tps.fotoPetugas.length > 0 && !tps.fotoPetugasUploaded ? `
|
|
||||||
<button type="button" class="tps-btn-upload-petugas w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
|
|
||||||
Upload ${tps.fotoPetugas.length} Foto Petugas
|
|
||||||
</button>
|
|
||||||
` : tps.fotoPetugasUploaded ? `
|
|
||||||
<div class="text-center text-xs text-green-600 font-bold py-2">
|
|
||||||
✓ Foto petugas sudah diupload
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Nama Petugas</label>
|
|
||||||
<input type="text" class="tps-nama-petugas w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Masukkan nama petugas" value="${tps.namaPetugas}" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<a href="@Url.Action("Batal", "DetailPenjemputan")" class="w-1/3 text-center bg-red-500 text-white py-3 rounded-xl font-bold text-sm">Batal</a>
|
|
||||||
<button type="submit" class="w-2/3 bg-upst text-white py-3 rounded-xl font-bold text-sm hover:brightness-110">Submit${showTpsName ? ' ' + tps.name : ''}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
attachTpsFormListeners();
|
|
||||||
restoreTpsTimbanganItems();
|
|
||||||
restorePhotoPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function restorePhotoPreview() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
const previewKedatangan = form.querySelector('.tps-preview-kedatangan');
|
|
||||||
if (previewKedatangan && tps.fotoKedatangan.length > 0) {
|
|
||||||
renderStoredPhotos(tps.fotoKedatangan, previewKedatangan);
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewPetugas = form.querySelector('.tps-preview-petugas');
|
|
||||||
if (previewPetugas && tps.fotoPetugas.length > 0) {
|
|
||||||
renderStoredPhotos(tps.fotoPetugas, previewPetugas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStoredPhotos(files, container) {
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.className = 'space-y-2';
|
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
|
|
||||||
|
|
||||||
const imageUrl = URL.createObjectURL(file);
|
|
||||||
const safeName = file.name.replace(/"/g, '"');
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="h-44 bg-black/80">
|
|
||||||
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
|
|
||||||
</div>
|
|
||||||
<div class="px-2 py-1 bg-white">
|
|
||||||
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
|
|
||||||
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const img = item.querySelector('.preview-multi-image');
|
|
||||||
if (img) {
|
|
||||||
img.onload = function() {
|
|
||||||
URL.revokeObjectURL(imageUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
container.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachTpsFormListeners() {
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
|
|
||||||
const fotoKedatanganInput = form.querySelector('.tps-foto-kedatangan');
|
|
||||||
const fotoPetugasInput = form.querySelector('.tps-foto-petugas');
|
|
||||||
const namaPetugasInput = form.querySelector('.tps-nama-petugas');
|
|
||||||
const btnAddTimbangan = form.querySelector('.tps-btn-add-timbangan');
|
|
||||||
const toggleDebug = form.querySelector('.tps-toggle-debug-crop');
|
|
||||||
|
|
||||||
fotoKedatanganInput.addEventListener('change', function() {
|
|
||||||
tps.fotoKedatangan = Array.from(this.files);
|
|
||||||
tps.fotoKedatanganUploaded = false;
|
|
||||||
updateWaktuKedatangan();
|
|
||||||
updateMultiPreview(this, form.querySelector('.tps-preview-kedatangan'));
|
|
||||||
renderTpsForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
fotoPetugasInput.addEventListener('change', function() {
|
|
||||||
tps.fotoPetugas = Array.from(this.files);
|
|
||||||
tps.fotoPetugasUploaded = false;
|
|
||||||
updateMultiPreview(this, form.querySelector('.tps-preview-petugas'));
|
|
||||||
renderTpsForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
namaPetugasInput.addEventListener('input', function() {
|
|
||||||
tps.namaPetugas = this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
btnAddTimbangan.addEventListener('click', function() {
|
|
||||||
createTimbanganItem(form.querySelector('.tps-timbangan-repeater'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const btnUploadKedatangan = form.querySelector('.tps-btn-upload-kedatangan');
|
|
||||||
if (btnUploadKedatangan) {
|
|
||||||
btnUploadKedatangan.addEventListener('click', function() {
|
|
||||||
uploadFotoKedatangan();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const btnUploadPetugas = form.querySelector('.tps-btn-upload-petugas');
|
|
||||||
if (btnUploadPetugas) {
|
|
||||||
btnUploadPetugas.addEventListener('click', function() {
|
|
||||||
uploadFotoPetugas();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
submitTpsData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreTpsTimbanganItems() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
|
||||||
|
|
||||||
if (tps.timbangan.length === 0) {
|
|
||||||
createTimbanganItem(repeater);
|
|
||||||
} else {
|
|
||||||
tps.timbangan.forEach(timb => {
|
|
||||||
const item = createTimbanganItem(repeater, timb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWaktuKedatangan() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const now = new Date();
|
|
||||||
const formatted = now.toLocaleString('id-ID', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
});
|
|
||||||
tps.waktuKedatangan = formatted;
|
|
||||||
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
const displayWaktu = form.querySelector('.tps-waktu-kedatangan');
|
|
||||||
if (displayWaktu) displayWaktu.value = formatted;
|
|
||||||
|
|
||||||
getLocationUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function reverseGeocode(lat, lng) {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
const address = data.display_name || `${lat}, ${lng}`;
|
|
||||||
tps.latitude = lat;
|
|
||||||
tps.longitude = lng;
|
|
||||||
tps.alamatJalan = address;
|
|
||||||
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (form) {
|
|
||||||
const latInput = form.querySelector('.tps-display-latitude');
|
|
||||||
const lngInput = form.querySelector('.tps-display-longitude');
|
|
||||||
if (latInput) latInput.value = lat;
|
|
||||||
if (lngInput) lngInput.value = lng;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
tps.latitude = lat;
|
|
||||||
tps.longitude = lng;
|
|
||||||
tps.alamatJalan = `${lat}, ${lng}`;
|
|
||||||
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (form) {
|
|
||||||
const latInput = form.querySelector('.tps-display-latitude');
|
|
||||||
const lngInput = form.querySelector('.tps-display-longitude');
|
|
||||||
if (latInput) latInput.value = lat;
|
|
||||||
if (lngInput) lngInput.value = lng;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocationUpdate() {
|
|
||||||
if (!('geolocation' in navigator)) return;
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
function(position) {
|
|
||||||
const lat = position.coords.latitude.toFixed(6);
|
|
||||||
const lng = position.coords.longitude.toFixed(6);
|
|
||||||
reverseGeocode(lat, lng);
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
console.log('Lokasi tidak diizinkan');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFileSize(bytes) {
|
|
||||||
const mb = bytes / (1024 * 1024);
|
|
||||||
return `${mb.toFixed(2)} MB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMultiPreview(input, previewContainer) {
|
|
||||||
if (!input || !previewContainer) return;
|
|
||||||
|
|
||||||
previewContainer.innerHTML = '';
|
|
||||||
previewContainer.className = 'space-y-2';
|
|
||||||
|
|
||||||
if (!input.files || input.files.length === 0) return;
|
|
||||||
|
|
||||||
Array.from(input.files).forEach((file, index) => {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'rounded-xl border border-gray-200 overflow-hidden bg-black';
|
|
||||||
|
|
||||||
const imageUrl = URL.createObjectURL(file);
|
|
||||||
const safeName = file.name.replace(/"/g, '"');
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="h-44 bg-black/80">
|
|
||||||
<img src="${imageUrl}" alt="Preview ${index + 1}" class="w-full h-full object-contain preview-multi-image" />
|
|
||||||
</div>
|
|
||||||
<div class="px-2 py-1 bg-white">
|
|
||||||
<p class="text-[11px] font-semibold text-gray-700 truncate">${index + 1}. ${safeName}</p>
|
|
||||||
<p class="text-[10px] text-gray-500">${formatFileSize(file.size)}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const img = item.querySelector('.preview-multi-image');
|
|
||||||
if (img) {
|
|
||||||
img.onload = function() {
|
|
||||||
URL.revokeObjectURL(imageUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
previewContainer.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWeightDisplay(value) {
|
|
||||||
if (isNaN(value)) return '0,00';
|
|
||||||
return value.toFixed(2).replace('.', ',');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseWeightInput(value) {
|
|
||||||
if (!value) return 0;
|
|
||||||
const cleaned = value.toString().trim().replace(/\s/g, '').replace(',', '.');
|
|
||||||
const parsed = parseFloat(cleaned);
|
|
||||||
return isNaN(parsed) ? 0 : parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFileAsImage(file) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = function() {
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
resolve(img);
|
|
||||||
};
|
|
||||||
img.onerror = function() {
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
reject(new Error('Gagal membaca gambar.'));
|
|
||||||
};
|
|
||||||
img.src = objectUrl;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCropCanvas(img, area) {
|
|
||||||
const sx = Math.max(0, Math.floor(img.width * area.x));
|
|
||||||
const sy = Math.max(0, Math.floor(img.height * area.y));
|
|
||||||
const sw = Math.max(1, Math.floor(img.width * area.w));
|
|
||||||
const sh = Math.max(1, Math.floor(img.height * area.h));
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const scale = 2.2;
|
|
||||||
canvas.width = Math.max(1, Math.floor(sw * scale));
|
|
||||||
canvas.height = Math.max(1, Math.floor(sh * scale));
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) return canvas;
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
function canvasToJpegFile(canvas, fileName) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
canvas.toBlob(function(blob) {
|
|
||||||
if (!blob) {
|
|
||||||
reject(new Error('Gagal membuat crop image.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(new File([blob], fileName, { type: 'image/jpeg' }));
|
|
||||||
}, 'image/jpeg', 0.92);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestOpenRouterWeight(imageFile) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('Foto', imageFile);
|
|
||||||
const response = await fetch('/upst/detail-penjemputan/ocr-timbangan', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(result.message || 'Request OCR gagal.');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function autoFillWeight(file, weightInput, ocrInfoEl) {
|
|
||||||
let guessedWeight = 0;
|
|
||||||
weightInput.placeholder = 'Membaca angka dari foto...';
|
|
||||||
if (ocrInfoEl) ocrInfoEl.textContent = 'AI OCR: memproses gambar...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const img = await readFileAsImage(file);
|
|
||||||
let bestRawText = '';
|
|
||||||
let isSuccess = false;
|
|
||||||
|
|
||||||
for (const area of OCR_AREAS) {
|
|
||||||
const cropCanvas = createCropCanvas(img, area);
|
|
||||||
const cropFile = await canvasToJpegFile(cropCanvas, `crop-${area.id}.jpg`);
|
|
||||||
const aiResult = await requestOpenRouterWeight(cropFile);
|
|
||||||
|
|
||||||
if (aiResult && aiResult.success && aiResult.weight) {
|
|
||||||
guessedWeight = parseWeightInput(aiResult.weight);
|
|
||||||
bestRawText = aiResult.raw || aiResult.weight;
|
|
||||||
isSuccess = guessedWeight > 0;
|
|
||||||
if (isSuccess) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aiResult && aiResult.raw) {
|
|
||||||
bestRawText = aiResult.raw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ocrInfoEl) {
|
|
||||||
const cleaned = (bestRawText || '').replace(/\s+/g, ' ').trim();
|
|
||||||
ocrInfoEl.textContent = isSuccess
|
|
||||||
? `AI OCR terbaca: ${cleaned}`
|
|
||||||
: (cleaned ? `AI OCR tidak valid: ${cleaned}` : 'AI OCR tidak menemukan angka valid.');
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
guessedWeight = 0;
|
|
||||||
if (ocrInfoEl) ocrInfoEl.textContent = 'AI OCR gagal diproses.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guessedWeight > 0) {
|
|
||||||
weightInput.value = formatWeightDisplay(guessedWeight);
|
|
||||||
weightInput.placeholder = 'Berat terdeteksi otomatis';
|
|
||||||
} else {
|
|
||||||
weightInput.placeholder = 'Tidak terbaca otomatis, isi manual';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTpsTotalTimbangan() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
let total = 0.0;
|
|
||||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
|
||||||
const items = repeater.querySelectorAll('.timbangan-item');
|
|
||||||
|
|
||||||
items.forEach(function(item) {
|
|
||||||
const weightInput = item.querySelector('.input-berat-timbangan-value');
|
|
||||||
if (weightInput) {
|
|
||||||
const value = parseWeightInput(weightInput.value || '0');
|
|
||||||
total += value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tps.totalTimbangan = total;
|
|
||||||
|
|
||||||
const displayTotal = form.querySelector('.tps-display-total');
|
|
||||||
if (displayTotal) displayTotal.textContent = formatWeightDisplay(total);
|
|
||||||
|
|
||||||
updateGrandTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGrandTotal() {
|
|
||||||
let grandTotal = 0;
|
|
||||||
tpsData.forEach(tps => {
|
|
||||||
grandTotal += tps.totalTimbangan;
|
|
||||||
});
|
|
||||||
if (grandTotalDisplay) {
|
|
||||||
grandTotalDisplay.textContent = formatWeightDisplay(grandTotal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTimbanganItem(repeater, existingData = null) {
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'timbangan-item rounded-2xl border border-gray-200 p-3 space-y-2 bg-gray-50';
|
|
||||||
|
|
||||||
const weight = existingData ? existingData.weight : 0;
|
|
||||||
const hasFile = existingData && existingData.file;
|
|
||||||
const isUploaded = existingData && existingData.uploaded;
|
|
||||||
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<p class="text-xs font-bold text-gray-600">Item Timbangan</p>
|
|
||||||
<button type="button" class="btn-remove-timbangan text-[11px] font-bold text-red-500">Hapus</button>
|
|
||||||
</div>
|
|
||||||
<input type="file" name="FotoTimbangan" accept="image/*" class="input-foto-timbangan block w-full text-sm text-gray-700 border border-gray-200 rounded-xl p-2 file:mr-3 file:rounded-lg file:border-0 file:bg-upst file:px-3 file:py-2 file:text-xs file:font-bold file:text-white" />
|
|
||||||
<div class="${hasFile ? '' : 'hidden'} input-preview-wrap relative rounded-xl overflow-hidden border border-gray-200 bg-black">
|
|
||||||
<img class="input-preview-image w-full h-44 object-contain" alt="Preview foto timbangan" />
|
|
||||||
<div class="input-crop-overlay absolute inset-0 pointer-events-none"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-[11px] text-gray-500 input-ocr-info">${hasFile ? 'OCR: diproses.' : 'OCR: belum diproses.'}</p>
|
|
||||||
${hasFile && !isUploaded ? `
|
|
||||||
<button type="button" class="btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110">
|
|
||||||
Upload Foto Timbangan Ini
|
|
||||||
</button>
|
|
||||||
` : isUploaded ? `
|
|
||||||
<div class="text-center text-xs text-green-600 font-bold py-2">
|
|
||||||
✓ Foto timbangan sudah diupload
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Berat (kg)</label>
|
|
||||||
<input type="text" inputmode="decimal" class="input-berat-timbangan-display w-full rounded-xl border border-gray-200 px-3 py-2 text-sm" placeholder="Contoh: 54,45" value="${weight > 0 ? formatWeightDisplay(weight) : ''}" />
|
|
||||||
<input type="hidden" class="input-berat-timbangan-value" value="${weight.toFixed(2)}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
|
||||||
const previewWrap = item.querySelector('.input-preview-wrap');
|
|
||||||
const previewImage = item.querySelector('.input-preview-image');
|
|
||||||
const cropOverlay = item.querySelector('.input-crop-overlay');
|
|
||||||
const ocrInfoEl = item.querySelector('.input-ocr-info');
|
|
||||||
const weightInputDisplay = item.querySelector('.input-berat-timbangan-display');
|
|
||||||
const weightInputValue = item.querySelector('.input-berat-timbangan-value');
|
|
||||||
const removeBtn = item.querySelector('.btn-remove-timbangan');
|
|
||||||
|
|
||||||
if (existingData && existingData.file) {
|
|
||||||
const localUrl = URL.createObjectURL(existingData.file);
|
|
||||||
previewImage.src = localUrl;
|
|
||||||
previewImage.onload = function() {
|
|
||||||
URL.revokeObjectURL(localUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', async function() {
|
|
||||||
if (fileInput.files && fileInput.files[0]) {
|
|
||||||
const localUrl = URL.createObjectURL(fileInput.files[0]);
|
|
||||||
previewImage.src = localUrl;
|
|
||||||
previewWrap.classList.remove('hidden');
|
|
||||||
previewImage.onload = function() {
|
|
||||||
URL.revokeObjectURL(localUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
await autoFillWeight(fileInput.files[0], weightInputDisplay, ocrInfoEl);
|
|
||||||
const parsed = parseWeightInput(weightInputDisplay.value);
|
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const itemIndex = Array.from(repeater.children).indexOf(item);
|
|
||||||
if (itemIndex >= 0 && tps.timbangan[itemIndex]) {
|
|
||||||
tps.timbangan[itemIndex].uploaded = false;
|
|
||||||
|
|
||||||
const existingUploadBtn = item.querySelector('.btn-upload-timbangan');
|
|
||||||
if (!existingUploadBtn) {
|
|
||||||
const ocrInfo = item.querySelector('.input-ocr-info');
|
|
||||||
const uploadBtn = document.createElement('button');
|
|
||||||
uploadBtn.type = 'button';
|
|
||||||
uploadBtn.className = 'btn-upload-timbangan w-full bg-blue-500 text-white py-2 rounded-xl font-bold text-xs hover:brightness-110';
|
|
||||||
uploadBtn.textContent = 'Upload Foto Timbangan Ini';
|
|
||||||
uploadBtn.addEventListener('click', function() {
|
|
||||||
uploadSingleFotoTimbangan(itemIndex);
|
|
||||||
});
|
|
||||||
ocrInfo.parentNode.insertBefore(uploadBtn, ocrInfo.nextSibling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
weightInputDisplay.addEventListener('input', function() {
|
|
||||||
const cleaned = this.value.replace(/[^0-9.,]/g, '');
|
|
||||||
this.value = cleaned;
|
|
||||||
const parsed = parseWeightInput(cleaned);
|
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
});
|
|
||||||
|
|
||||||
weightInputDisplay.addEventListener('blur', function() {
|
|
||||||
const parsed = parseWeightInput(this.value);
|
|
||||||
if (parsed > 0) {
|
|
||||||
this.value = formatWeightDisplay(parsed);
|
|
||||||
weightInputValue.value = parsed.toFixed(2);
|
|
||||||
} else {
|
|
||||||
this.value = '';
|
|
||||||
weightInputValue.value = '0.00';
|
|
||||||
}
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
});
|
|
||||||
|
|
||||||
removeBtn.addEventListener('click', function() {
|
|
||||||
item.remove();
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
const repeater = form ? form.querySelector('.tps-timbangan-repeater') : null;
|
|
||||||
if (repeater && repeater.children.length === 0) {
|
|
||||||
createTimbanganItem(repeater);
|
|
||||||
}
|
|
||||||
updateTpsTotalTimbangan();
|
|
||||||
syncTimbanganToTpsData();
|
|
||||||
});
|
|
||||||
|
|
||||||
const btnUploadTimbangan = item.querySelector('.btn-upload-timbangan');
|
|
||||||
if (btnUploadTimbangan) {
|
|
||||||
btnUploadTimbangan.addEventListener('click', function() {
|
|
||||||
const itemIndex = Array.from(repeater.children).indexOf(item);
|
|
||||||
uploadSingleFotoTimbangan(itemIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
repeater.appendChild(item);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncTimbanganToTpsData() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
|
||||||
const items = repeater.querySelectorAll('.timbangan-item');
|
|
||||||
|
|
||||||
tps.timbangan = [];
|
|
||||||
items.forEach(item => {
|
|
||||||
const fileInput = item.querySelector('.input-foto-timbangan');
|
|
||||||
const weightValue = item.querySelector('.input-berat-timbangan-value');
|
|
||||||
|
|
||||||
const existingIndex = tps.timbangan.length;
|
|
||||||
const existingData = tps.timbangan[existingIndex];
|
|
||||||
|
|
||||||
tps.timbangan.push({
|
|
||||||
file: fileInput.files[0] || (existingData ? existingData.file : null),
|
|
||||||
weight: parseWeightInput(weightValue.value),
|
|
||||||
uploaded: existingData ? existingData.uploaded : false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadSingleFotoTimbangan(itemIndex) {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
|
|
||||||
if (!tps.timbangan[itemIndex] || !tps.timbangan[itemIndex].file) {
|
|
||||||
alert('Belum ada foto timbangan yang dipilih!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timbanganItem = tps.timbangan[itemIndex];
|
|
||||||
|
|
||||||
alert(`Upload foto timbangan #${itemIndex + 1} untuk ${tps.name}\nBerat: ${timbanganItem.weight} kg\n(Implementasi upload ke server)`);
|
|
||||||
|
|
||||||
timbanganItem.uploaded = true;
|
|
||||||
|
|
||||||
const form = tpsContentContainer.querySelector('form');
|
|
||||||
const repeater = form.querySelector('.tps-timbangan-repeater');
|
|
||||||
const items = repeater.querySelectorAll('.timbangan-item');
|
|
||||||
const targetItem = items[itemIndex];
|
|
||||||
|
|
||||||
if (targetItem) {
|
|
||||||
const uploadBtn = targetItem.querySelector('.btn-upload-timbangan');
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ocrInfo = targetItem.querySelector('.input-ocr-info');
|
|
||||||
if (ocrInfo && !targetItem.querySelector('.upload-success-message')) {
|
|
||||||
const successMsg = document.createElement('div');
|
|
||||||
successMsg.className = 'text-center text-xs text-green-600 font-bold py-2 upload-success-message';
|
|
||||||
successMsg.textContent = '✓ Foto timbangan sudah diupload';
|
|
||||||
ocrInfo.parentNode.insertBefore(successMsg, ocrInfo.nextSibling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFotoKedatangan() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
if (tps.fotoKedatangan.length === 0) {
|
|
||||||
alert('Belum ada foto kedatangan yang dipilih!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(`Upload ${tps.fotoKedatangan.length} foto kedatangan untuk ${tps.name}\n(Implementasi upload ke server)`);
|
|
||||||
|
|
||||||
tps.fotoKedatanganUploaded = true;
|
|
||||||
renderTpsForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFotoPetugas() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
if (tps.fotoPetugas.length === 0) {
|
|
||||||
alert('Belum ada foto petugas yang dipilih!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert(`Upload ${tps.fotoPetugas.length} foto petugas untuk ${tps.name}\n(Implementasi upload ke server)`);
|
|
||||||
|
|
||||||
tps.fotoPetugasUploaded = true;
|
|
||||||
renderTpsForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitTpsData() {
|
|
||||||
const tps = tpsData[activeTpsIndex];
|
|
||||||
|
|
||||||
if (!tps.fotoKedatangan.length) {
|
|
||||||
alert('Foto kedatangan belum diupload!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!tps.timbangan.length) {
|
|
||||||
alert('Belum ada data timbangan!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!tps.fotoPetugas.length) {
|
|
||||||
alert('Foto petugas belum diupload!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!tps.namaPetugas.trim()) {
|
|
||||||
alert('Nama petugas belum diisi!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('TpsName', tps.name);
|
|
||||||
formData.append('Latitude', tps.latitude);
|
|
||||||
formData.append('Longitude', tps.longitude);
|
|
||||||
formData.append('AlamatJalan', tps.alamatJalan);
|
|
||||||
formData.append('WaktuKedatangan', tps.waktuKedatangan);
|
|
||||||
formData.append('TotalTimbangan', tps.totalTimbangan);
|
|
||||||
formData.append('NamaPetugas', tps.namaPetugas);
|
|
||||||
|
|
||||||
tps.fotoKedatangan.forEach((file, i) => {
|
|
||||||
formData.append(`FotoKedatangan`, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
tps.timbangan.forEach((timb, i) => {
|
|
||||||
if (timb.file) formData.append(`FotoTimbangan`, timb.file);
|
|
||||||
formData.append(`BeratTimbangan`, timb.weight);
|
|
||||||
});
|
|
||||||
|
|
||||||
tps.fotoPetugas.forEach((file, i) => {
|
|
||||||
formData.append(`FotoPetugas`, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(`Submit data ${tps.name}:\n- Lokasi: ${tps.alamatJalan}\n- Total: ${tps.totalTimbangan} kg\n- Petugas: ${tps.namaPetugas}\n\n(Implementasi POST ke server)`);
|
|
||||||
|
|
||||||
tps.submitted = true;
|
|
||||||
if (selectedTpsList.length > 1) {
|
|
||||||
renderTabs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeLocation(['TPS A', 'TPS B', 'TPS C']);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</register-block>
|
|
||||||
|
|
@ -65,7 +65,6 @@
|
||||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||||
--color-blue-800: oklch(42.4% 0.199 265.638);
|
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||||
--color-blue-900: oklch(37.9% 0.146 265.522);
|
|
||||||
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
--color-indigo-50: oklch(96.2% 0.018 272.314);
|
||||||
--color-indigo-100: oklch(93% 0.034 272.788);
|
--color-indigo-100: oklch(93% 0.034 272.788);
|
||||||
--color-indigo-300: oklch(78.5% 0.115 274.713);
|
--color-indigo-300: oklch(78.5% 0.115 274.713);
|
||||||
|
|
@ -109,8 +108,6 @@
|
||||||
--text-xl--line-height: calc(1.75 / 1.25);
|
--text-xl--line-height: calc(1.75 / 1.25);
|
||||||
--text-2xl: 1.5rem;
|
--text-2xl: 1.5rem;
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
--text-3xl: 1.875rem;
|
|
||||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
|
|
@ -350,9 +347,6 @@
|
||||||
.-top-4 {
|
.-top-4 {
|
||||||
top: calc(var(--spacing) * -4);
|
top: calc(var(--spacing) * -4);
|
||||||
}
|
}
|
||||||
.-top-5 {
|
|
||||||
top: calc(var(--spacing) * -5);
|
|
||||||
}
|
|
||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
|
@ -422,9 +416,6 @@
|
||||||
.right-full {
|
.right-full {
|
||||||
right: 100%;
|
right: 100%;
|
||||||
}
|
}
|
||||||
.-bottom-0 {
|
|
||||||
bottom: calc(var(--spacing) * -0);
|
|
||||||
}
|
|
||||||
.-bottom-0\.5 {
|
.-bottom-0\.5 {
|
||||||
bottom: calc(var(--spacing) * -0.5);
|
bottom: calc(var(--spacing) * -0.5);
|
||||||
}
|
}
|
||||||
|
|
@ -461,9 +452,6 @@
|
||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.left-1 {
|
|
||||||
left: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.left-1\/2 {
|
.left-1\/2 {
|
||||||
left: calc(1/2 * 100%);
|
left: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
|
|
@ -857,12 +845,6 @@
|
||||||
.table-row {
|
.table-row {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
}
|
}
|
||||||
.aspect-square {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
}
|
|
||||||
.h-0 {
|
|
||||||
height: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.h-0\.5 {
|
.h-0\.5 {
|
||||||
height: calc(var(--spacing) * 0.5);
|
height: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
|
|
@ -968,9 +950,6 @@
|
||||||
.w-1 {
|
.w-1 {
|
||||||
width: calc(var(--spacing) * 1);
|
width: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.w-1\/2 {
|
|
||||||
width: calc(1/2 * 100%);
|
|
||||||
}
|
|
||||||
.w-1\/3 {
|
.w-1\/3 {
|
||||||
width: calc(1/3 * 100%);
|
width: calc(1/3 * 100%);
|
||||||
}
|
}
|
||||||
|
|
@ -1106,10 +1085,6 @@
|
||||||
.border-collapse {
|
.border-collapse {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.-translate-x-1 {
|
|
||||||
--tw-translate-x: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-x-1\/2 {
|
.-translate-x-1\/2 {
|
||||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
|
@ -1122,10 +1097,6 @@
|
||||||
--tw-translate-x: calc(var(--spacing) * 16);
|
--tw-translate-x: calc(var(--spacing) * 16);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
.-translate-y-1 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-y-1\/2 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
|
@ -1186,9 +1157,6 @@
|
||||||
.grid-cols-2 {
|
.grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.grid-cols-4 {
|
.grid-cols-4 {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
@ -1246,13 +1214,6 @@
|
||||||
.gap-5 {
|
.gap-5 {
|
||||||
gap: calc(var(--spacing) * 5);
|
gap: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
.space-y-0 {
|
|
||||||
:where(& > :not(:last-child)) {
|
|
||||||
--tw-space-y-reverse: 0;
|
|
||||||
margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
|
|
||||||
margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.space-y-0\.5 {
|
.space-y-0\.5 {
|
||||||
:where(& > :not(:last-child)) {
|
:where(& > :not(:last-child)) {
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
|
|
@ -1538,9 +1499,6 @@
|
||||||
.border-green-400 {
|
.border-green-400 {
|
||||||
border-color: var(--color-green-400);
|
border-color: var(--color-green-400);
|
||||||
}
|
}
|
||||||
.border-green-600 {
|
|
||||||
border-color: var(--color-green-600);
|
|
||||||
}
|
|
||||||
.border-green-600\/20 {
|
.border-green-600\/20 {
|
||||||
border-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent);
|
border-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|
@ -1643,12 +1601,6 @@
|
||||||
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
|
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bg-black\/70 {
|
|
||||||
background-color: color-mix(in srgb, #000 70%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-black) 70%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-black\/75 {
|
.bg-black\/75 {
|
||||||
background-color: color-mix(in srgb, #000 75%, transparent);
|
background-color: color-mix(in srgb, #000 75%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|
@ -1673,9 +1625,6 @@
|
||||||
.bg-blue-500 {
|
.bg-blue-500 {
|
||||||
background-color: var(--color-blue-500);
|
background-color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
.bg-cyan-400 {
|
|
||||||
background-color: var(--color-cyan-400);
|
|
||||||
}
|
|
||||||
.bg-cyan-400\/10 {
|
.bg-cyan-400\/10 {
|
||||||
background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent);
|
background-color: color-mix(in srgb, oklch(78.9% 0.154 211.53) 10%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|
@ -1736,9 +1685,6 @@
|
||||||
.bg-indigo-300 {
|
.bg-indigo-300 {
|
||||||
background-color: var(--color-indigo-300);
|
background-color: var(--color-indigo-300);
|
||||||
}
|
}
|
||||||
.bg-lime-500 {
|
|
||||||
background-color: var(--color-lime-500);
|
|
||||||
}
|
|
||||||
.bg-lime-500\/15 {
|
.bg-lime-500\/15 {
|
||||||
background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent);
|
background-color: color-mix(in srgb, oklch(76.8% 0.233 130.85) 15%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|
@ -1760,9 +1706,6 @@
|
||||||
.bg-orange-500 {
|
.bg-orange-500 {
|
||||||
background-color: var(--color-orange-500);
|
background-color: var(--color-orange-500);
|
||||||
}
|
}
|
||||||
.bg-orange-700 {
|
|
||||||
background-color: var(--color-orange-700);
|
|
||||||
}
|
|
||||||
.bg-orange-700\/30 {
|
.bg-orange-700\/30 {
|
||||||
background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent);
|
background-color: color-mix(in srgb, oklch(55.3% 0.195 38.402) 30%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
|
@ -2171,9 +2114,6 @@
|
||||||
.py-16 {
|
.py-16 {
|
||||||
padding-block: calc(var(--spacing) * 16);
|
padding-block: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
.py-\[1px\] {
|
|
||||||
padding-block: 1px;
|
|
||||||
}
|
|
||||||
.ps-0 {
|
.ps-0 {
|
||||||
padding-inline-start: calc(var(--spacing) * 0);
|
padding-inline-start: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
|
@ -2898,31 +2838,16 @@
|
||||||
background-color: var(--color-red-100);
|
background-color: var(--color-red-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file\:mr-2 {
|
|
||||||
&::file-selector-button {
|
|
||||||
margin-right: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file\:mr-3 {
|
.file\:mr-3 {
|
||||||
&::file-selector-button {
|
&::file-selector-button {
|
||||||
margin-right: calc(var(--spacing) * 3);
|
margin-right: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file\:mr-4 {
|
|
||||||
&::file-selector-button {
|
|
||||||
margin-right: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file\:rounded-lg {
|
.file\:rounded-lg {
|
||||||
&::file-selector-button {
|
&::file-selector-button {
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file\:rounded-xl {
|
|
||||||
&::file-selector-button {
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file\:border-0 {
|
.file\:border-0 {
|
||||||
&::file-selector-button {
|
&::file-selector-button {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
|
|
@ -2934,16 +2859,6 @@
|
||||||
padding-inline: calc(var(--spacing) * 3);
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file\:px-4 {
|
|
||||||
&::file-selector-button {
|
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file\:py-1 {
|
|
||||||
&::file-selector-button {
|
|
||||||
padding-block: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.file\:py-2 {
|
.file\:py-2 {
|
||||||
&::file-selector-button {
|
&::file-selector-button {
|
||||||
padding-block: calc(var(--spacing) * 2);
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
|
@ -3236,16 +3151,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:file\:brightness-110 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
&::file-selector-button {
|
|
||||||
--tw-brightness: brightness(110%);
|
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:border-gray-500 {
|
.focus\:border-gray-500 {
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--color-gray-500);
|
border-color: var(--color-gray-500);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue