update: admin scan select2, convert tanggal

main
marszayn 2025-08-12 09:51:22 +07:00
parent d8684446e6
commit 74ab38ce6b
4 changed files with 551 additions and 74 deletions

View File

@ -39,17 +39,35 @@ public class SpjAdminController : Controller
[HttpPost("scan/process/{id:guid}", Name = "Admin_Scan_Process")]
public IActionResult Process(Guid id)
{
// Dummy rule: hanya GUID dummy yang dianggap valid
if (id == DummySpjGuid)
// GUID yang sudah pernah di scan
var alreadyScannedGuid = new Guid("550e8400-e29b-41d4-a716-446655440007");
if (id == alreadyScannedGuid)
{
return Json(new {
success = false,
message = "SPJ ini sudah pernah di scan pada 20 Agustus 2024."
});
}
var validGuids = new[]
{
DummySpjGuid, // GUID asli yang sudah ada
new Guid("550e8400-e29b-41d4-a716-446655440001"), // Ahmad Supriyadi
new Guid("550e8400-e29b-41d4-a716-446655440002"), // Budi Santoso
new Guid("550e8400-e29b-41d4-a716-446655440003"), // Candra Wijaya
new Guid("550e8400-e29b-41d4-a716-446655440004"), // Dedi Kurniawan
new Guid("550e8400-e29b-41d4-a716-446655440006") // Eko Prasetyo gua coba gagalin
};
if (validGuids.Contains(id))
{
return Json(new { success = true, data = new { id, status = "Valid" } });
}
return Json(new { success = false, message = "SPJ tidak ditemukan." });
}
// Resolve kode SPJ (contoh: "SPJ/08-2025/PKM/000519") menjadi GUID.
// Catatan: Saat ini dummy (development): setiap format SPJ yang valid bakal ke GUID baru.
// Integrasi produksi: ganti dengan query DB untuk mencari SPJ berdasarkan nomor, lalu kembalikan Id GUID yang sebenarnya.
[ValidateAntiForgeryToken]
[HttpPost("scan/resolve", Name = "Admin_Scan_Resolve")]
public IActionResult Resolve([FromForm] string code)
@ -79,13 +97,141 @@ public class SpjAdminController : Controller
return Json(new { success = false, message = "Format kode tidak dikenali." });
}
// Dummy mapping: jika persis sesuai nomor di atas, kembalikan GUID tetap; selain itu anggap tidak ditemukan
if (string.Equals(code, DummySpjNumber, StringComparison.OrdinalIgnoreCase))
// Mapping SPJ code ke GUID (semua dummy data)
var spjMapping = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase)
{
return Json(new { success = true, id = DummySpjGuid });
{ DummySpjNumber, DummySpjGuid },
{ "SPJ/01-2024/TRN/240815", new Guid("550e8400-e29b-41d4-a716-446655440001") },
{ "SPJ/02-2024/TRN/240816", new Guid("550e8400-e29b-41d4-a716-446655440002") },
{ "SPJ/03-2024/TRN/240817", new Guid("550e8400-e29b-41d4-a716-446655440003") },
{ "SPJ/04-2024/TRN/240818", new Guid("550e8400-e29b-41d4-a716-446655440004") },
{ "SPJ/05-2024/TRN/240819", new Guid("550e8400-e29b-41d4-a716-446655440005") },
{ "SPJ/06-2024/TRN/240820", new Guid("550e8400-e29b-41d4-a716-446655440007") } // Sudah pernah di scan
};
if (spjMapping.TryGetValue(code, out var mappedGuid))
{
return Json(new { success = true, id = mappedGuid });
}
return Json(new { success = false, message = "SPJ tidak ditemukan." });
}
[HttpGet("search-spj")]
public IActionResult SearchSpj(string q, int page = 1, int pageSize = 20)
{
try
{
// Dummy data buat testing
var dummySpjList = new[]
{
new {
id = "550e8400-e29b-41d4-a716-446655440001",
spjCode = "SPJ/01-2024/TRN/240815",
driverName = "Ahmad Supriyadi",
platNomor = "B 1234 ABC",
nomorPintu = "001"
},
new {
id = "550e8400-e29b-41d4-a716-446655440002",
spjCode = "SPJ/02-2024/TRN/240816",
driverName = "Budi Santoso",
platNomor = "B 5678 DEF",
nomorPintu = "002"
},
new {
id = "550e8400-e29b-41d4-a716-446655440003",
spjCode = "SPJ/03-2024/TRN/240817",
driverName = "Candra Wijaya",
platNomor = "B 9012 GHI",
nomorPintu = "003"
},
new {
id = "550e8400-e29b-41d4-a716-446655440004",
spjCode = "SPJ/04-2024/TRN/240818",
driverName = "Dedi Kurniawan",
platNomor = "B 3456 JKL",
nomorPintu = "004"
},
new {
id = "550e8400-e29b-41d4-a716-446655440005",
spjCode = "SPJ/05-2024/TRN/240819",
driverName = "Eko Prasetyo",
platNomor = "B 7890 MNO",
nomorPintu = "005"
},
new {
id = DummySpjGuid.ToString(),
spjCode = DummySpjNumber,
driverName = "Fahmi Rahman",
platNomor = "B 2468 PQR",
nomorPintu = "006"
},
new {
id = "550e8400-e29b-41d4-a716-446655440007",
spjCode = "SPJ/06-2024/TRN/240820",
driverName = "Gita Sari",
platNomor = "B 1357 STU",
nomorPintu = "007"
}
};
var filteredData = dummySpjList.AsQueryable();
if (!string.IsNullOrWhiteSpace(q))
{
q = q.ToLower();
filteredData = dummySpjList.Where(s =>
s.spjCode.ToLower().Contains(q) ||
s.driverName.ToLower().Contains(q) ||
s.platNomor.ToLower().Contains(q) ||
s.nomorPintu.ToLower().Contains(q)
).AsQueryable();
}
var total = filteredData.Count();
var items = filteredData
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return Json(new {
items = items,
hasMore = (page * pageSize) < total
});
/*
// PRODUCTION VERSION - Uncomment untuk production - tambahin sendiri yaa
var query = _context.SpjData
.Where(s => s.SpjCode.Contains(q) ||
s.DriverName.Contains(q) ||
s.PlatNomor.Contains(q) ||
s.NomorPintu.Contains(q))
.OrderBy(s => s.SpjCode);
var total = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(s => new {
id = s.Id,
spjCode = s.SpjCode,
driverName = s.DriverName,
platNomor = s.PlatNomor,
nomorPintu = s.NomorPintu
})
.ToListAsync();
return Json(new {
items = items,
hasMore = (page * pageSize) < total
});
*/
}
catch (Exception ex)
{
return Json(new { error = ex.Message });
}
}
}

View File

@ -5,6 +5,109 @@
@section Styles {
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.select2-container {
width: 100% !important;
}
.select2-container--default .select2-selection--single {
height: 42px !important;
border: 1px solid #d1d5db !important;
border-radius: 0.5rem !important;
padding: 0 12px !important;
display: flex !important;
align-items: center !important;
font-size: 14px !important;
}
.select2-container--default .select2-selection--single:focus {
border-color: #f97316 !important;
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2) !important;
outline: none !important;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #374151 !important;
line-height: normal !important;
padding: 0 !important;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #9ca3af !important;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 40px !important;
right: 8px !important;
}
.select2-dropdown {
border: 1px solid #d1d5db !important;
border-radius: 0.5rem !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
}
.select2-container--default .select2-results__option {
padding: 12px 16px !important;
font-size: 14px !important;
border-bottom: 1px solid #f3f4f6 !important;
}
.select2-container--default .select2-results__option:last-child {
border-bottom: none !important;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #f97316 !important;
color: white !important;
}
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #fed7aa !important;
color: #ea580c !important;
}
.select2-search--dropdown .select2-search__field {
border: 1px solid #d1d5db !important;
border-radius: 0.375rem !important;
padding: 8px 12px !important;
font-size: 14px !important;
}
.select2-search--dropdown .select2-search__field:focus {
border-color: #f97316 !important;
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2) !important;
outline: none !important;
}
.select2-results__message {
padding: 12px 16px !important;
color: #6b7280 !important;
font-size: 14px !important;
}
.spj-option {
display: flex;
flex-direction: column;
gap: 4px;
}
.spj-code {
font-weight: 600;
color: #1f2937;
font-family: 'Courier New', monospace;
}
.spj-details {
font-size: 12px;
color: #6b7280;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
}
@ -107,14 +210,19 @@
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
<form id="manual-form" method="post" action="#" novalidate>
@Html.AntiForgeryToken()
<div class="flex gap-2">
<input type="text"
id="manual-barcode"
name="barcode"
placeholder="Masukkan kode SPJ manual"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent">
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="w-5 h-5" data-lucide="search"></i>
<div class="space-y-3">
<div>
<select id="manual-barcode-select"
name="barcode"
class="w-full"
style="width: 100%;">
<option value="">Ketik untuk mencari SPJ...</option>
</select>
</div>
<button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-lg transition-colors">
<i class="w-5 h-5 inline mr-2" data-lucide="search"></i>
Proses SPJ
</button>
</div>
</form>
@ -163,6 +271,7 @@
<register-block dynamic-section="scripts" key="jsScan">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
<script>
@ -175,6 +284,86 @@
</script>
<script>
function initializeSelect2() {
$('#manual-barcode-select').select2({
placeholder: 'Ketik untuk mencari SPJ...',
allowClear: true,
minimumInputLength: 2,
ajax: {
url: '@Url.Action("SearchSpj", "SpjAdmin")',
dataType: 'json',
delay: 300,
data: function (params) {
return {
q: params.term,
page: params.page || 1,
pageSize: 20
};
},
processResults: function (data, params) {
params.page = params.page || 1;
return {
results: data.items.map(function(item) {
return {
id: item.id,
text: item.spjCode,
driverName: item.driverName,
platNomor: item.platNomor,
nomorPintu: item.nomorPintu
};
}),
pagination: {
more: data.hasMore
}
};
},
cache: true
},
templateResult: formatSpjOption,
templateSelection: formatSpjSelection,
escapeMarkup: function(markup) {
return markup;
}
});
}
function formatSpjOption(spj) {
if (spj.loading) {
return spj.text;
}
if (!spj.driverName) {
return spj.text;
}
return $(
'<div class="spj-option">' +
'<div class="spj-code">' + spj.text + '</div>' +
'<div class="spj-details">' +
'<span>' + spj.driverName + '</span>' +
'<span class="bg-orange-300 rounded-full px-2 py-1 text-xs text-gray-500">' + spj.nomorPintu + '</span>' +
'</div>' +
'<div style="font-size: 11px; color: #9ca3af; font-family: monospace;">' + spj.platNomor + '</div>' +
'</div>'
);
}
function formatSpjSelection(spj) {
return spj.text || spj.id;
}
function updateManualFormHandler() {
const manualInput = document.getElementById('manual-barcode-select');
const scanner = window.barcodeScanner;
if (manualInput && scanner) {
scanner.manualInput = manualInput;
}
}
class BarcodeScanner {
constructor() {
this.isScanning = false;
@ -201,7 +390,7 @@
this.confirmBtn = document.getElementById('confirm-scan');
this.retryBtn = document.getElementById('retry-scan');
this.manualForm = document.getElementById('manual-form');
this.manualInput = document.getElementById('manual-barcode');
this.manualInput = document.getElementById('manual-barcode-select');
this.permissionInfo = document.getElementById('permission-info');
this.permissionDenied = document.getElementById('permission-denied');
this.scanningIndicator = document.getElementById('scanning-indicator');
@ -373,6 +562,9 @@
this.detectedCode = code;
this.isProcessing = true;
// Simpan SPJ code asli untuk ditampilkan di modal
this.originalSpjCode = code;
try {
if (this.html5QrCode && typeof this.html5QrCode.pause === 'function') {
@ -423,6 +615,8 @@
confirmScan() {
if (this.detectedCode) {
// Simpan SPJ code asli untuk ditampilkan di modal
this.originalSpjCode = this.detectedCode;
this.processScanCode(this.detectedCode);
}
}
@ -467,11 +661,14 @@
try {
let guid = null;
const trimmed = (code || '').trim();
if (!this.originalSpjCode) {
this.originalSpjCode = trimmed;
}
if (this.isGuid(trimmed)) {
guid = trimmed;
} else if (this.isSpjCode(trimmed)) {
// minta backend resolve SPJ ke GUID
const rs = await this.resolveSpj(trimmed);
if (!rs || rs.success !== true || !rs.id) {
throw new Error((rs && rs.message) ? rs.message : 'Gagal resolve SPJ ke GUID.');
@ -481,16 +678,20 @@
throw new Error('Format kode tidak dikenali. Masukkan GUID atau nomor SPJ yang valid.');
}
// POST GUID ke process endpoint
const resp = await this.postGuid(guid);
console.debug('process response', resp);
if (resp && resp.success) {
const now = new Date();
const tanggal = now.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
const waktu = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
this.showSuccessModal(trimmed, tanggal, waktu);
this.showSuccessModal(this.originalSpjCode, tanggal, waktu);
} else {
this.showModal('error', 'SPJ Tidak Ditemukan', (resp && resp.message) ? resp.message : 'Kode SPJ tidak ditemukan dalam database.', true);
const errorMessage = (resp && resp.message) ? resp.message : 'Kode SPJ tidak ditemukan dalam database.';
if (errorMessage.toLowerCase().includes('sudah pernah di scan')) {
this.showAlreadyScannedModal(this.originalSpjCode);
} else {
this.showModal('error', 'SPJ Tidak Ditemukan', errorMessage, true);
}
}
} catch (xhrOrErr) {
let message = '';
@ -505,6 +706,8 @@
}
console.error('processScanCode error', xhrOrErr);
this.showModal('error', 'Error', message, true);
} finally {
this.originalSpjCode = null;
}
}
@ -515,13 +718,11 @@
this.hidePermissionMessages();
this.detectedCode = null;
// Jika scanner sudah aktif, tidak perlu restart
if (this.isScanning) {
console.log("Scanner sudah aktif, tidak perlu restart");
return;
}
// Hanya restart jika scanner tidak aktif
setTimeout(() => {
this.startScanner();
}, 500);
@ -530,9 +731,11 @@
handleManualSubmit(e) {
e.preventDefault();
const code = this.manualInput.value.trim();
const code = $('#manual-barcode-select').val();
const selectedData = $('#manual-barcode-select').select2('data')[0];
if (!code) {
this.showModal('error', 'Input Kosong', 'Silakan masukkan kode SPJ.', true);
this.showModal('error', 'Input Kosong', 'Silakan pilih SPJ dari daftar.', true);
return;
}
@ -541,6 +744,15 @@
return;
}
console.log('Selected SPJ:', {
id: selectedData?.id,
spjCode: selectedData?.text,
driver: selectedData?.driverName,
platNomor: selectedData?.platNomor,
nomorPintu: selectedData?.nomorPintu
});
this.originalSpjCode = selectedData?.text || code;
this.processScanCode(code);
}
@ -600,6 +812,7 @@
const iconHtml = {
'success': '<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center shadow-lg"><svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg></div>',
'error': '<div class="w-20 h-20 mx-auto bg-red-100 rounded-full flex items-center justify-center shadow-lg"><svg class="w-10 h-10 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></div>',
'warning': '<div class="w-20 h-20 mx-auto bg-yellow-100 rounded-full flex items-center justify-center shadow-lg"><svg class="w-10 h-10 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg></div>',
'loading': '<div class="w-20 h-20 mx-auto bg-blue-100 rounded-full flex items-center justify-center shadow-lg"><div class="loading-spinner-modal"></div></div>'
};
@ -677,7 +890,59 @@
setTimeout(() => {
this.hideModal();
this.hideResult();
this.manualInput.value = '';
$('#manual-barcode-select').val(null).trigger('change');
this.resumeAfterDelay(300);
}, 2000);
}
showAlreadyScannedModal(code) {
const warningIcon = `
<div class="w-28 h-28 mx-auto bg-gradient-to-r from-yellow-400 to-amber-500 rounded-full flex items-center justify-center shadow-xl mb-6 animate-pulse">
<svg class="w-16 h-16 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
`;
const warningContent = `
<div class="bg-gradient-to-br from-yellow-50 via-amber-50 to-orange-50 rounded-2xl p-6 border border-yellow-200 shadow-inner">
<div class="flex items-center justify-center mb-4">
<div class="bg-gradient-to-r from-yellow-500 to-amber-600 text-white rounded-xl px-4 py-2 shadow-lg">
<span class="font-mono font-bold text-xl tracking-wider">${code}</span>
</div>
</div>
<div class="space-y-4">
<div class="bg-white rounded-xl p-4 shadow-sm border border-yellow-100">
<div class="text-center">
<div class="flex flex-col gap-2 items-center justify-center mb-2">
<svg class="w-6 h-6 text-yellow-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
<span class="text-yellow-700 font-bold text-lg">SPJ SUDAH DISCAN</span>
</div>
<p class="text-yellow-600 text-sm mt-2">SPJ ini telah diproses sebelumnya</p>
</div>
</div>
</div>
</div>
`;
$('#modal-icon').html(warningIcon);
$('#modal-title').html('<span class="text-xl font-bold bg-gradient-to-r from-yellow-600 to-amber-600 bg-clip-text text-transparent">SPJ Sudah Discan</span>');
$('#modal-message').html(warningContent);
$('#modal-close').hide();
$('#scan-modal').removeClass('hidden');
$('#scan-modal').addClass('flex');
$('#scan-modal').css('opacity', '0').animate({'opacity': '1'}, 300);
$('#scan-modal .bg-white').css('transform', 'scale(0.8)').animate({
'transform': 'scale(1)'
}, 300);
setTimeout(() => {
this.hideModal();
this.resumeAfterDelay(300);
}, 2000);
}
@ -769,9 +1034,12 @@
}
});
initializeSelect2();
function waitForLibrary() {
if (typeof Html5Qrcode !== 'undefined') {
new BarcodeScanner();
window.barcodeScanner = new BarcodeScanner();
updateManualFormHandler();
} else {
setTimeout(waitForLibrary, 500);
}

View File

@ -180,7 +180,7 @@
</div>
<!-- Waktu Masuk dan Keluar -->
<div class="grid grid-cols-2 gap-3">
<div class="grid grid-cols-1 gap-3">
<div>
<label for="WaktuMasuk" class="block text-sm font-medium text-gray-700 mb-1">Masuk</label>
<input
@ -188,7 +188,7 @@
id="WaktuMasuk"
name="WaktuMasuk"
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
placeholder="04 Aug 2025, 08:13:51"
placeholder="2025-08-04, 08:13:51"
/>
</div>
<div>
@ -198,7 +198,7 @@
id="WaktuKeluar"
name="WaktuKeluar"
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
placeholder="04 Aug 2025, 14:35:10"
placeholder="2025-08-04, 14:35:10"
/>
</div>
</div>
@ -576,26 +576,23 @@
let weightOut = '';
let weightNett = '';
// Enhanced receipt number detection - prioritize month prefix patterns
const receiptPatterns = [
/(\d{2})_(\d{6,})/gi, // "08_7999566" - underscore pattern (highest priority)
/(\d{2})\s+(\d{6,})/gi, // "08 7999566" - space pattern
/(?:no.*struk|nomor.*struk|receipt)[\s.:]*(\d{6,})/gi, // Context-based pattern
/(?:^|\s)(\d{6,10})(?:\s|$)/g, // Standalone number pattern
/(\d{2})_(\d{6,})/gi,
/(\d{2})\s+(\d{6,})/gi,
/(?:no.*struk|nomor.*struk|receipt)[\s.:]*(\d{6,})/gi,
/(?:^|\s)(\d{6,10})(?:\s|$)/g,
];
for (const line of lines) {
console.log(`Processing line for receipt: "${line}"`);
// Pattern 1: Month_Number format (08_7999566) - HIGHEST PRIORITY
const monthUnderscoreMatch = line.match(/(\d{2})_(\d{6,})/);
if (monthUnderscoreMatch && monthUnderscoreMatch[2]) {
receiptNumber = monthUnderscoreMatch[2]; // Take ONLY the number after underscore
receiptNumber = monthUnderscoreMatch[2];
console.log(`Found receipt number: ${receiptNumber} using month-underscore pattern (removed prefix: ${monthUnderscoreMatch[1]}_)`);
break;
}
// Pattern 2: Month Number format (08 7999566)
const monthSpaceMatch = line.match(/(\d{2})\s+(\d{6,})/);
if (monthSpaceMatch && monthSpaceMatch[2]) {
receiptNumber = monthSpaceMatch[2]; // Take ONLY the number after space
@ -603,7 +600,6 @@
break;
}
// Pattern 3: Context-based patterns (fallback)
for (const pattern of receiptPatterns.slice(2)) {
const matches = [...line.matchAll(pattern)];
for (const match of matches) {
@ -671,10 +667,8 @@
}
if (!truckNumber) {
// Ambil semua karakter setelah label "No Truk" tanpa filter khusus
for (const line of lines) {
if (line.toLowerCase().includes('no truk')) {
// Contoh: "No Truk : B 9501 TOQ"
const match = line.match(/no\s*truk\s*:?\s*(.+)/i);
if (match && match[1]) {
truckNumber = match[1].trim();
@ -683,7 +677,6 @@
}
}
}
// Jika tidak ditemukan, fallback ke pattern lama (awalan B)
if (!truckNumber) {
for (const line of lines) {
const match = line.match(/\bB\s*\d{3,5}\s*[A-Z]{2,4}\b/i);
@ -697,14 +690,12 @@
console.log('Truck detection result:', truckNumber);
// Assignment detection - ambil semua karakter setelah label "Penugasan"
for (const line of lines) {
console.log(`Processing line for assignment: "${line}"`);
if (line.toLowerCase().includes('penugasan')) {
console.log('Found penugasan line:', line);
// Contoh: "Penugasan : JAKARTA TIMUR" atau "Penugasan: UPST DLH"
const match = line.match(/penugasan\s*:?\s*(.+)/i);
if (match && match[1]) {
assignment = match[1].trim();
@ -716,7 +707,6 @@
console.log('Assignment detection result:', assignment);
// Enhanced time patterns - prioritize YYYY-MM-DD HH:MM:SS format
const timePatterns = [
/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/gi, // 2025-08-02 12:35:34 (highest priority)
/(\d{1,2}\s+\w{3}\s+\d{4},\s*\d{1,2}:\d{2}:\d{2})/gi, // 04 Aug 2025, 08:13:51
@ -725,63 +715,102 @@
/(\d{1,2}-\d{1,2}-\d{4}\s+\d{1,2}:\d{2}:\d{2})/gi, // 04-08-2025 08:13:51
];
const convertDateFormat = (dateString) => {
const monthMap = {
'jan': '01', 'january': '01', 'januari': '01',
'feb': '02', 'february': '02', 'februari': '02',
'mar': '03', 'march': '03', 'maret': '03',
'apr': '04', 'april': '04', 'april': '04',
'may': '05', 'may': '05', 'mei': '05',
'jun': '06', 'june': '06', 'juni': '06',
'jul': '07', 'july': '07', 'juli': '07',
'aug': '08', 'august': '08', 'agustus': '08',
'sep': '09', 'september': '09', 'september': '09',
'oct': '10', 'october': '10', 'oktober': '10',
'nov': '11', 'november': '11', 'november': '11',
'dec': '12', 'december': '12', 'desember': '12'
};
const match = dateString.match(/(\d{1,2})\s+(\w{3,})\s+(\d{4}),?\s*(\d{1,2}:\d{2}:\d{2})/i);
if (match) {
const day = match[1].padStart(2, '0');
const monthName = match[2].toLowerCase();
const year = match[3];
const time = match[4];
const month = monthMap[monthName];
if (month) {
return `${year}-${month}-${day}, ${time}`;
}
}
return dateString;
};
for (const line of lines) {
console.log(`Processing line for time: "${line}"`);
// Entry time - look for "Masuk :" pattern
if (line.toLowerCase().includes('masuk') && line.includes(':')) {
console.log('Found masuk line:', line);
// First try to find exact YYYY-MM-DD HH:MM:SS format after "Masuk :"
const masukMatch = line.match(/masuk\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i);
if (masukMatch && masukMatch[1]) {
entryTime = masukMatch[1].trim();
console.log(`Found entry time via masuk pattern: ${entryTime} (format: YYYY-MM-DD HH:MM:SS)`);
} else {
// Fallback to general patterns
for (const pattern of timePatterns) {
const match = line.match(pattern);
if (match && match[1]) {
entryTime = match[1].trim();
console.log(`Found entry time: ${entryTime}`);
const rawTime = match[1].trim();
entryTime = convertDateFormat(rawTime);
console.log(`Found entry time: ${rawTime} -> converted to: ${entryTime}`);
break;
}
}
}
// Also try to extract after "Masuk : "
const masukMatch = line.match(/masuk\s*:\s*(.+)/i);
if (masukMatch && masukMatch[1] && !entryTime) {
entryTime = masukMatch[1].trim();
console.log(`Found entry time via masuk pattern: ${entryTime}`);
if (!entryTime) {
const masukMatch = line.match(/masuk\s*:\s*(.+)/i);
if (masukMatch && masukMatch[1]) {
const rawTime = masukMatch[1].trim();
entryTime = convertDateFormat(rawTime);
console.log(`Found entry time via masuk pattern: ${rawTime} -> converted to: ${entryTime}`);
}
}
}
// Exit time - look for "Keluar :" pattern
if (line.toLowerCase().includes('keluar') && line.includes(':')) {
console.log('Found keluar line:', line);
// First try to find exact YYYY-MM-DD HH:MM:SS format after "Keluar :"
const keluarMatch = line.match(/keluar\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i);
if (keluarMatch && keluarMatch[1]) {
exitTime = keluarMatch[1].trim();
console.log(`Found exit time via keluar pattern: ${exitTime} (format: YYYY-MM-DD HH:MM:SS)`);
} else {
// Fallback to general patterns
for (const pattern of timePatterns) {
const match = line.match(pattern);
if (match && match[1]) {
exitTime = match[1].trim();
console.log(`Found exit time: ${exitTime}`);
const rawTime = match[1].trim();
exitTime = convertDateFormat(rawTime);
console.log(`Found exit time: ${rawTime} -> converted to: ${exitTime}`);
break;
}
}
}
if (!exitTime) {
const keluarMatch = line.match(/keluar\s*:\s*(.+)/i);
if (keluarMatch && keluarMatch[1]) {
const rawTime = keluarMatch[1].trim();
exitTime = convertDateFormat(rawTime);
console.log(`Found exit time via keluar pattern: ${rawTime} -> converted to: ${exitTime}`);
}
}
}
}
console.log('Time detection results:', { entryTime, exitTime });
// Enhanced weight patterns - more specific for Indonesian receipts
const weightPatterns = [
/berat\s+masuk\s*:\s*(\d+)\s*kg/gi, // Berat Masuk : 23280 kg
/berat\s+keluar\s*:\s*(\d+)\s*kg/gi, // Berat Keluar : 13540 kg
@ -820,11 +849,9 @@
}
}
// Berat Keluar - look for exact pattern "Berat Keluar : 13540 kg"
if (lowerLine.includes('berat keluar')) {
console.log('Found berat keluar line:', line);
// Try specific pattern first
const specificMatch = line.match(/berat\s+keluar\s*:\s*(\d+)\s*kg/gi);
if (specificMatch) {
const numbers = specificMatch[0].match(/(\d+)/);
@ -833,7 +860,6 @@
console.log(`Found weight out via specific pattern: ${weightOut} (removed kg)`);
}
} else {
// Try general patterns
for (const pattern of weightPatterns) {
const match = line.match(pattern);
if (match && match[1] && parseInt(match[1]) > 1000) {

View File

@ -27,13 +27,16 @@
--color-amber-50: oklch(98.7% 0.022 95.277);
--color-amber-200: oklch(92.4% 0.12 95.746);
--color-amber-400: oklch(82.8% 0.189 84.429);
--color-amber-500: oklch(76.9% 0.188 70.08);
--color-amber-600: oklch(66.6% 0.179 58.318);
--color-amber-700: oklch(55.5% 0.163 48.998);
--color-yellow-50: oklch(98.7% 0.026 102.212);
--color-yellow-100: oklch(97.3% 0.071 103.193);
--color-yellow-200: oklch(94.5% 0.129 101.54);
--color-yellow-400: oklch(85.2% 0.199 91.936);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-yellow-600: oklch(68.1% 0.162 75.834);
--color-yellow-700: oklch(55.4% 0.135 66.442);
--color-yellow-800: oklch(47.6% 0.114 61.907);
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-100: oklch(96.2% 0.044 156.743);
@ -837,6 +840,9 @@
.h-50 {
height: calc(var(--spacing) * 50);
}
.h-60 {
height: calc(var(--spacing) * 60);
}
.h-64 {
height: calc(var(--spacing) * 64);
}
@ -1388,6 +1394,9 @@
border-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.border-yellow-100 {
border-color: var(--color-yellow-100);
}
.border-yellow-200 {
border-color: var(--color-yellow-200);
}
@ -1538,6 +1547,9 @@
.bg-yellow-50 {
background-color: var(--color-yellow-50);
}
.bg-yellow-100 {
background-color: var(--color-yellow-100);
}
.bg-yellow-400 {
background-color: var(--color-yellow-400);
}
@ -1632,6 +1644,27 @@
--tw-gradient-from: var(--color-white);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-yellow-50 {
--tw-gradient-from: var(--color-yellow-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-yellow-400 {
--tw-gradient-from: var(--color-yellow-400);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-yellow-500 {
--tw-gradient-from: var(--color-yellow-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-yellow-600 {
--tw-gradient-from: var(--color-yellow-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.via-amber-50 {
--tw-gradient-via: var(--color-amber-50);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-blue-50 {
--tw-gradient-via: var(--color-blue-50);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
@ -1662,6 +1695,14 @@
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.to-amber-500 {
--tw-gradient-to: var(--color-amber-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-amber-600 {
--tw-gradient-to: var(--color-amber-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-blue-200 {
--tw-gradient-to: var(--color-blue-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -1761,6 +1802,9 @@
.bg-clip-text {
background-clip: text;
}
.bg-center {
background-position: center;
}
.object-contain {
object-fit: contain;
}
@ -2139,6 +2183,9 @@
.text-yellow-600 {
color: var(--color-yellow-600);
}
.text-yellow-700 {
color: var(--color-yellow-700);
}
.text-yellow-800 {
color: var(--color-yellow-800);
}
@ -2636,16 +2683,6 @@
outline-style: none;
}
}
.md\:max-w-md {
@media (width >= 48rem) {
max-width: var(--container-md);
}
}
.md\:max-w-sm {
@media (width >= 48rem) {
max-width: var(--container-sm);
}
}
.lg\:max-w-sm {
@media (width >= 64rem) {
max-width: var(--container-sm);