update queuestore dari API

This commit is contained in:
bagus-arie05
2026-01-29 08:11:30 +07:00
parent 9f0f6a75c9
commit 19633afde1
8 changed files with 1037 additions and 431 deletions

No files matched your search

+179 -44
View File
@@ -39,13 +39,12 @@
@open-penunjang="openPenunjangDialog"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="apiQuota || 150"
:total-quota="loketQuota"
:used-quota="quotaUsed"
:menunggu-count="menungguCount"
:has-next="!!nextPatient || menungguCount > 0"
:menunggu-count="filteredMenungguCount"
:has-next="!!nextPatient || filteredMenungguCount > 0"
@call="handleCall"
/>
</div>
@@ -55,12 +54,11 @@
<v-col cols="12" md="7">
<v-card class="patient-data-container" elevation="0">
<v-card-text class="pa-4">
<!-- Header with Filters -->
<div class="data-header mb-4">
<div class="section-label mb-3">PANGGIL ANTREAN</div>
<div v-if="menungguCount > 0" class="waiting-badge mb-2">
<div v-if="filteredMenungguCount > 0" class="waiting-badge mb-2">
<v-icon start size="14">mdi-clock-outline</v-icon>
{{ menungguCount }} Antrean Menunggu
{{ filteredMenungguCount }} Antrean Menunggu
</div>
<div class="filters">
@@ -81,11 +79,11 @@
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
v-model:selected-fast-track="selectedFastTrack"
:di-loket-count="diLoketCount"
:di-loket-count="filteredDiLoketCount"
:diproses-count="currentProcessingPatient ? 1 : 0"
:waiting-count="waitingCount"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:waiting-count="filteredWaitingCount"
:terlambat-count="filteredTerlambatCount"
:pending-count="filteredPendingCount"
:show-diproses="false"
:fast-track-options="fastTrackOptions"
@action="handleTableAction"
@@ -241,6 +239,7 @@ const { printTicketFromPatient } = useThermalPrint();
// Broadcast Channel
let broadcastChannel = null;
let resetCheckInterval = null;
const loketId = computed(() => route.params.id);
const loketName = computed(() => {
@@ -307,6 +306,22 @@ onMounted(async () => {
// 3. Fetch specific quota/loket data from API to ensure fresh counts
// This matches MasterLoket.vue implementation
await fetchQuotaFromAPI();
// 4. Fetch patient data for this loket
await fetchPatientsForCurrentLoket();
// 5. Periodic check for daily reset (2 AM)
resetCheckInterval = setInterval(() => {
const didReset = queueStore.checkAndResetDaily();
if (didReset) {
console.log('🕒 [AdminLoket] 2 AM threshold reached. Data reset performed.');
fetchPatientsForCurrentLoket(); // Refresh after reset
}
}, 60000); // Check every minute
});
onUnmounted(() => {
if (resetCheckInterval) clearInterval(resetCheckInterval);
});
const apiQuota = ref(null);
@@ -333,6 +348,25 @@ const fetchQuotaFromAPI = async () => {
}
};
// Fetch patient data for current loket
const fetchPatientsForCurrentLoket = async () => {
const targetId = parseInt(loketId.value);
const currentLoket = loketStore.getLoketById(targetId);
// Check if loket is REGULER (not EKSEKUTIF)
const isEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' ||
currentLoket?.tipeLoket === 'EKSEKUTIF' ||
(currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF');
if (!isEksekutif) {
// For REGULER loket, fetch from API
console.log(`🔄 [AdminLoket] Fetching patients for REGULER loket ${targetId}...`);
await queueStore.fetchPatientsForLoket(targetId);
} else {
console.log(`📋 [AdminLoket] Using seed data for EKSEKUTIF loket ${targetId}`);
}
};
const currentDate = ref(
new Date().toLocaleDateString("id-ID", {
weekday: "long",
@@ -380,6 +414,12 @@ const fastTrackOptions = computed(() => {
return uniqueTracks.sort();
});
// Loket quota from configuration
const loketQuota = computed(() => {
const currentLoket = loketStore.getLoketById(parseInt(loketId.value));
return currentLoket?.kuota || apiQuota.value || 150;
});
// Combine all patients with status - PRESERVE ALL PROPERTIES
const allPatientsForStage = computed(() => {
const currentPatientNo = currentProcessingPatient.value?.no;
@@ -387,28 +427,59 @@ const allPatientsForStage = computed(() => {
const currentLoket = loketStore.getLoketById(parseInt(targetLoketId));
console.log('🔍 [AdminLoket] currentLoket:', currentLoket);
console.log('🔍 [AdminLoket] targetLoketId:', targetLoketId);
const allowedServices = currentLoket?.pelayanan || [];
// Helper to check if patient belongs to this loket
// Check if loket is EKSEKUTIF or REGULER
const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' ||
currentLoket?.tipeLoket === 'EKSEKUTIF' ||
(currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF');
let basePatients = [];
if (isLoketEksekutif) {
// For EKSEKUTIF loket, use seed data (filter from allPatients)
// IMPORTANT: Do NOT include menungguPatients - they are only called via QueueActionsCard
console.log('📋 [AdminLoket] Using seed data for EKSEKUTIF loket');
basePatients = diLoketPatients.value.concat(waitingPatients.value, terlambatPatients.value, pendingPatients.value);
} else {
// For REGULER loket, use API data
console.log('🌐 [AdminLoket] Using API data for REGULER loket');
const apiPatients = queueStore.apiPatientsPerLoket[targetLoketId] || [];
basePatients = apiPatients;
}
// Helper to check if patient belongs to this loket (for seed data only)
const isPatientForThisLoket = (p) => {
// 0. STRICT FILTER: Payment Type vs Loket Type
const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' || currentLoket?.jenisloket === 'EKSEKUTIF' || (currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF');
const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') || (p.pembayaran || '').toUpperCase().includes('VIP'); // Check payment type
// Only check for EKSEKUTIF patients (seed data)
const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') ||
(p.pembayaran || '').toUpperCase().includes('VIP');
if (isLoketEksekutif) {
// Loket Eksekutif HANYA melayani pasien Eksekutif
if (!isPatientEksekutif) return false;
// For EKSEKUTIF loket: accept all EKSEKUTIF patients
// EKSEKUTIF lokets typically serve ALL clinics for executive patients
// So we don't need to check kodeKlinik matching
// Only check explicit loketId assignment if present
if (p.loketId) {
return String(p.loketId) === String(targetLoketId);
}
// If no loketId assigned, accept all EKSEKUTIF patients
return true;
} else {
// Loket Reguler TIDAK melayani pasien Eksekutif
// Loket Reguler TIDAK melayani pasien Eksekutif (this shouldn't happen with API data)
if (isPatientEksekutif) return false;
}
// For REGULER seed data only: check loket assignment
// Priority 1: Explicit loketId match
if (p.loketId) {
return String(p.loketId) === String(targetLoketId);
}
// Priority 2: Service (kodeKlinik) match
const allowedServices = currentLoket?.pelayanan || [];
if (p.kodeKlinik) {
return allowedServices.includes(p.kodeKlinik);
}
@@ -426,38 +497,102 @@ const allPatientsForStage = computed(() => {
return false;
};
const diLoket = (diLoketPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: p.no === currentPatientNo ? "diproses" : "diloket",
}));
// If using seed data (EKSEKUTIF), filter by loket
// If using API data (REGULER), basePatients already contains only relevant patients
let combined = [];
const terlambat = (terlambatPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "terlambat",
}));
const pending = (pendingPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "pending",
}));
const waiting = (waitingPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "waiting",
}));
if (isLoketEksekutif) {
// Filter seed data by loket assignment
const diLoket = (diLoketPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: p.no === currentPatientNo ? "diproses" : "diloket",
}));
const terlambat = (terlambatPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "terlambat",
}));
const pending = (pendingPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "pending",
}));
const waiting = (waitingPatients.value || [])
.filter(isPatientForThisLoket)
.map((p) => ({
...p,
status: "waiting",
}));
const combined = [...diLoket, ...waiting, ...terlambat, ...pending];
combined = [...diLoket, ...waiting, ...terlambat, ...pending];
} else {
// Use API data, but EXCLUDE patients with status "menunggu"
// They should only be called via QueueActionsCard, not displayed in table
combined = basePatients
.filter(p => p.status !== 'menunggu') // IMPORTANT: Filter out menunggu
.map((p) => ({
...p,
status: p.no === currentPatientNo ? "diproses" : (p.status || "menunggu"),
}));
}
return combined;
});
// Computed status counts based on filtered allPatientsForStage
// These counts are ONLY for patients that belong to this specific loket
const filteredWaitingCount = computed(() => {
return allPatientsForStage.value.filter(p => p.status === 'waiting').length;
});
const filteredDiLoketCount = computed(() => {
return allPatientsForStage.value.filter(p => p.status === 'diloket' || p.status === 'di-loket').length;
});
const filteredTerlambatCount = computed(() => {
return allPatientsForStage.value.filter(p => p.status === 'terlambat').length;
});
const filteredPendingCount = computed(() => {
return allPatientsForStage.value.filter(p => p.status === 'pending').length;
});
// Menunggu count from API or seed data based on loket type
const filteredMenungguCount = computed(() => {
const targetLoketId = loketId.value;
const currentLoket = loketStore.getLoketById(parseInt(targetLoketId));
const isLoketEksekutif = currentLoket?.tipeloket === 'EKSEKUTIF' ||
currentLoket?.tipeLoket === 'EKSEKUTIF' ||
(currentLoket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF');
if (isLoketEksekutif) {
// For EKSEKUTIF, use seed data menunggu count (filtered by loket)
return menungguPatients.value.filter(p => {
const isPatientEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') ||
(p.pembayaran || '').toUpperCase().includes('VIP');
if (!isPatientEksekutif) return false;
// Accept all EKSEKUTIF patients if no explicit loketId
if (p.loketId) {
return String(p.loketId) === String(targetLoketId);
}
return true;
}).length;
} else {
// For REGULER, count from API data
const apiPatients = queueStore.apiPatientsPerLoket[targetLoketId] || [];
return apiPatients.filter(p => p.status === 'menunggu').length;
}
});
const waitingCount = computed(() => {
return (waitingPatients.value || []).length;
});
+66 -19
View File
@@ -13,27 +13,47 @@
<div
v-for="loket in loketList"
:key="loket.id"
class="loket-card"
@click="navigateToLoket(loket.id)"
:class="['loket-card', { 'loket-card-inactive': !loket.loketAktif }]"
@click="loket.loketAktif ? navigateToLoket(loket.id) : null"
>
<div class="loket-card-header">
<div class="loket-info">
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
<div class="loket-details">
<!-- Tipe Loket -->
<v-chip
size="x-small"
:class="['JKN', 'Reguler', 'REGULER'].includes(loket.pembayaran) ? 'chip-reguler' : 'chip-eksekutif'"
:class="['JKN', 'Reguler', 'REGULER'].includes(loket.tipeLoket || loket.tipeloket) ? 'chip-tipe-reguler' : 'chip-tipe-eksekutif'"
>
{{ loket.pembayaran }}
{{ loket.tipeLoket || loket.tipeloket || 'REGULER' }}
</v-chip>
<span class="kode-chip">ID: {{ loket.id }}</span>
<!-- Pembayaran (Multiple Chips) -->
<v-chip
v-for="(payment, idx) in (Array.isArray(loket.pembayaran) ? loket.pembayaran.slice(0, 2) : [loket.pembayaran])"
:key="idx"
size="x-small"
:color="loket.loketAktif ? 'success' : 'grey'"
class="chip-pembayaran"
>
{{ payment }}
</v-chip>
<v-chip
v-if="Array.isArray(loket.pembayaran) && loket.pembayaran.length > 2"
size="x-small"
class="chip-more-payment"
>
+{{ loket.pembayaran.length - 2 }}
</v-chip>
<!-- Status Tidak Aktif (only show when inactive) -->
<v-chip
v-if="!loket.loketAktif"
size="x-small"
color="error"
variant="flat"
class="ml-2"
>
{{ loket.loketAktif ? 'Aktif' : 'Non-Aktif' }}
Tidak Aktif
</v-chip>
</div>
</div>
@@ -165,6 +185,26 @@ const navigateToLoket = (id) => {
}
}
.loket-card-inactive {
background: var(--color-neutral-600) !important;
cursor: not-allowed !important;
opacity: 0.7;
&:hover {
transform: none !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
border-color: var(--color-neutral-400) !important;
}
.loket-name {
color: var(--color-neutral-100) !important;
}
.pelayanan-count {
color: var(--color-neutral-300) !important;
}
}
.loket-card-header {
display: flex;
align-items: center;
@@ -192,29 +232,36 @@ const navigateToLoket = (id) => {
margin-top: 4px;
}
.chip-reguler {
// Chip Tipe Loket
.chip-tipe-reguler {
background-color: #009262 !important;
color: var(--color-neutral-100) !important;
font-weight: 600;
font-size: 11px;
}
.chip-tipe-eksekutif {
background-color: #E67E22 !important;
color: var(--color-neutral-100) !important;
font-weight: 600;
font-size: 11px;
}
// Chip Pembayaran
.chip-pembayaran {
background-color: var(--color-primary-600) !important;
color: var(--color-neutral-100) !important;
font-weight: 600;
font-size: 11px;
}
.chip-eksekutif {
background-color: var(--color-secondary-600) !important;
.chip-more-payment {
background-color: var(--color-neutral-600) !important;
color: var(--color-neutral-100) !important;
font-weight: 600;
font-size: 11px;
}
.kode-chip {
font-size: 12px;
font-weight: 600;
color: var(--color-neutral-700);
background: var(--color-neutral-400);
padding: 2px 8px;
border-radius: 4px;
}
.loket-preview {
flex: 1;
}
+55 -24
View File
@@ -6,6 +6,9 @@
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
alt="RSUD Logo"
class="header-logo"
width="40"
height="40"
style="width: 40px; height: 40px; object-fit= contain;"
/>
</div>
<div class="header-content">
@@ -1086,18 +1089,32 @@ const registerPatient = async (visitType, paymentType, namaDokter, isFastTrack =
}
// Register patient to queueStore
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
visitType,
null,
'Shift 1',
namaDokter,
isFastTrack,
fastTrackData, // Pass fastTrackData (penanggungJawab, alasanFastTrack)
null, // Stop passing anjunganId as targetLoketId (fix routing bug)
null // Stop passing anjunganName as targetLoket name
);
let result;
if (!isEksekutif.value) {
// Create patient via API for REGULER
result = await queueStore.registerRegulerPatientViaApi(
selectedClinic.value,
paymentType,
visitType,
isFastTrack,
fastTrackData
);
} else {
// Existing logic for Eksekutif
result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
visitType,
null,
'Shift 1',
namaDokter,
isFastTrack,
fastTrackData, // Pass fastTrackData (penanggungJawab, alasanFastTrack)
null, // Stop passing anjunganId as targetLoketId (fix routing bug)
null // Stop passing anjunganName as targetLoket name
);
}
if (result && result.success && result.patient) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
@@ -1164,18 +1181,32 @@ const submitBooking = async () => {
return;
}
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
'JADWAL_LAIN',
bookingForm.value.date,
bookingForm.value.shift,
namaDokter,
false, // isFastTrack
null, // fastTrackData
null, // Stop passing anjunganId as targetLoketId (fix routing bug)
null // Stop passing anjunganName as targetLoket name
);
let result;
if (!isEksekutif.value) {
// Create patient via API for REGULER
// NOTE: API might not support date, so it will generate for today
result = await queueStore.registerRegulerPatientViaApi(
selectedClinic.value,
paymentType,
'JADWAL_LAIN',
false, // isFastTrack
null // fastTrackData
);
} else {
result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
'JADWAL_LAIN',
bookingForm.value.date,
bookingForm.value.shift,
namaDokter,
false, // isFastTrack
null, // fastTrackData
null, // Stop passing anjunganId as targetLoketId (fix routing bug)
null // Stop passing anjunganName as targetLoket name
);
}
if (result && result.success && result.patient) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
+3
View File
@@ -7,6 +7,9 @@
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
alt="RSUD Logo"
class="header-logo"
width="40"
height="40"
style="width: 40px; height: 40px; object-fit= contain;
/>
</div>
<div class="header-content">
+3 -3
View File
@@ -1366,7 +1366,7 @@ const klinikOptions = computed(() => {
const PATIENT_ID_STORAGE_KEY = 'checkin_patient_id_counter';
const QUEUE_NUMBER_STORAGE_KEY = 'checkin_queue_number_counters';
const LAST_RESET_TIME_KEY = 'checkin_last_reset_time';
const RESET_HOUR = 22; // Jam 10 malam (22:00)
const RESET_HOUR = 2; // Jam 2 pagi
const HISTORY_STORAGE_KEY = 'checkin_history';
// Check and reset daily at 10 PM (22:00)
@@ -2472,7 +2472,7 @@ const onDetect = async (decodedText: string) => {
const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || decodedText;
console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn);
console.log('🔍 Original QR data:', decodedText, '| Extracted barcode:', searchBarcode);
const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn);
const checkInResult = await queueStore.checkInPatient(patientBarcodeForCheckIn);
if (checkInResult.success && checkInResult.patient) {
// Check-in berhasil
@@ -2781,7 +2781,7 @@ const checkInManual = async () => {
const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || inputValue;
console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn);
console.log('🔍 Original input:', inputValue, '| Extracted barcode:', searchBarcode);
const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn);
const checkInResult = await queueStore.checkInPatient(patientBarcodeForCheckIn);
if (checkInResult.success && checkInResult.patient) {
// Check-in berhasil
+37 -10
View File
@@ -59,12 +59,30 @@
</v-chip>
</template>
<template #item.pembayaran="{ item }">
<template #item.tipeLoket="{ item }">
<v-chip
size="small"
:class="['JKN', 'Reguler', 'REGULER'].includes(item.pembayaran) ? 'chip-reguler' : 'chip-eksekutif'"
:class="['JKN', 'Reguler', 'REGULER'].includes(item.tipeLoket || item.tipeloket) ? 'chip-reguler' : 'chip-eksekutif'"
>
{{ item.pembayaran }}
{{ item.tipeLoket || item.tipeloket }}
</v-chip>
</template>
<template #item.pembayaran="{ item }">
<v-chip
v-for="(payment, idx) in (Array.isArray(item.pembayaran) ? item.pembayaran.slice(0, 2) : [item.pembayaran])"
:key="idx"
size="small"
class="mr-1 mb-1 chip-payment"
>
{{ payment }}
</v-chip>
<v-chip
v-if="Array.isArray(item.pembayaran) && item.pembayaran.length > 2"
size="small"
class="chip-neutral"
>
+{{ item.pembayaran.length - 2 }}
</v-chip>
</template>
@@ -181,12 +199,12 @@
<v-row dense>
<v-col cols="6">
<v-select
v-model="formData.pembayaran"
label="Pembayaran"
:items="['JKN', 'UMUM', 'EKSEKUTIF', 'SPM', 'JKMM', 'JAMPERSAL', 'T4', 'KARYAWAN']"
v-model="formData.tipeLoket"
label="Tipe Loket"
:items="['REGULER', 'EKSEKUTIF']"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Pembayaran harus dipilih']"
:rules="[v => !!v || 'Tipe Loket harus dipilih']"
hide-details="auto"
class="input-field"
/>
@@ -403,6 +421,7 @@ const loketHeaders = ref([
{ title: "Nama Loket", value: "namaLoket" },
{ title: "Kuota", value: "kuota" },
{ title: "Pelayanan", value: "pelayanan" },
{ title: "Tipe Loket", value: "tipeLoket" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Status Loket Aktif", value: "loketAktif" },
{ title: "Layar Informasi", value: "layarInformasi", sortable: false, width: "150px" },
@@ -414,7 +433,7 @@ const formData = ref({
namaLoket: '',
kuota: null,
statusPelayanan: '',
pembayaran: '',
tipeLoket: '',
pelayanan: [],
loketAktif: true,
});
@@ -432,7 +451,7 @@ const openEditDialog = (item) => {
namaLoket: item.namaLoket,
kuota: item.kuota,
statusPelayanan: item.statusPelayanan || 'RAWAT JALAN',
pembayaran: item.pembayaran,
tipeLoket: item.tipeLoket || item.tipeloket,
keterangan: item.keterangan,
pelayanan: [...item.pelayanan],
};
@@ -450,7 +469,7 @@ const resetForm = () => {
namaLoket: '',
kuota: null,
statusPelayanan: '',
pembayaran: '',
tipeLoket: '',
keterangan: '',
pelayanan: [],
};
@@ -666,6 +685,14 @@ $font-weight-semibold: 600;
font-weight: 500;
}
.chip-payment {
background-color: #3A61C9 !important;
color: #FFFFFF !important;
font-weight: 500;
font-size: 12px;
line-height: 16px;
}
.btn-edit {
background-color: $primary-600 !important;
color: $neutral-100 !important;
+71 -32
View File
@@ -99,7 +99,8 @@ export const useLoketStore = defineStore('loket', () => {
namaLoket: `LOKET ${i + 1} EKS`,
kuota: 500,
pelayanan: services,
pembayaran: 'EKSEKUTIF',
pembayaran: ['EKSEKUTIF'], // Changed to array for consistency
tipeLoket: 'EKSEKUTIF', // Add tipeLoket field for consistency
keterangan: 'ONLINE',
statusPelayanan: 'RAWAT JALAN',
source: 'local',
@@ -198,7 +199,8 @@ export const useLoketStore = defineStore('loket', () => {
id: newId,
no: newNo,
...loketPayload,
pembayaran: 'EKSEKUTIF', // Force EKSEKUTIF for local data
pembayaran: loketPayload.pembayaran || ['EKSEKUTIF'], // Ensure array format
tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support
source: 'local', // Mark as local data
loketAktif: loketPayload.loketAktif ?? true, // Default aktif
};
@@ -220,7 +222,10 @@ export const useLoketStore = defineStore('loket', () => {
...localLoketData.value[index],
...loketPayload,
source: 'local', // Ensure source stays local
pembayaran: 'EKSEKUTIF', // Ensure pembayaran stays EKSEKUTIF
pembayaran: loketPayload.pembayaran && Array.isArray(loketPayload.pembayaran)
? loketPayload.pembayaran
: ['EKSEKUTIF'], // Ensure array format
tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support
};
return { success: true, message: `Loket ${loketPayload.namaLoket} berhasil diupdate` };
}
@@ -271,8 +276,18 @@ export const useLoketStore = defineStore('loket', () => {
}
const hasData = apiLoketData.value && apiLoketData.value.length > 0;
if (hasData && !force && retryCount === 0) {
return { success: true, message: 'Data sudah tersedia' };
const hasCachedData = hasData && lastSyncTimestamp.value;
// If we have cached data and not forcing refresh, use cache
if (hasCachedData && !force && retryCount === 0) {
const cacheAge = Date.now() - new Date(lastSyncTimestamp.value).getTime();
const cacheAgeMinutes = Math.floor(cacheAge / 60000);
console.log(`📦 Using cached API data (${cacheAgeMinutes} minutes old, ${apiLoketData.value.length} items)`);
return {
success: true,
message: `Data tersedia dari cache (${cacheAgeMinutes} menit yang lalu)`,
cached: true
};
}
const performFetch = async () => {
@@ -318,11 +333,13 @@ export const useLoketStore = defineStore('loket', () => {
// Extract unique codes for 'pelayanan' field
const pelayananCodes = [...new Set(spesialisDetail.map(s => s.code))];
// Map payment types
const pembayaranLabel = (l.pembayaran || [])
// Map payment types as array for multiple chips display
const pembayaranArray = (l.pembayaran || [])
.map(p => p.pembayaran)
.filter(Boolean)
.join(', ') || 'JKN';
.filter(Boolean);
// If no payment types, default to ['JKN']
const pembayaran = pembayaranArray.length > 0 ? pembayaranArray : ['JKN'];
return {
id: id,
@@ -331,11 +348,12 @@ export const useLoketStore = defineStore('loket', () => {
kuota: parseInt(l.kuotaloket) || 100,
pelayanan: pelayananCodes,
_spesialisDetail: spesialisDetail,
pembayaran: pembayaranLabel,
pembayaran: pembayaran, // Now an array instead of joined string
tipeLoket: l.tipeloket || 'REGULER', // Map tipeloket to tipeLoket (capital L)
source: 'api',
loketAktif: l.loketaktif ?? true,
jenisloket: l.jenisloket,
tipeloket: l.tipeloket
tipeloket: l.tipeloket // Keep original for backward compatibility
};
});
@@ -360,27 +378,40 @@ export const useLoketStore = defineStore('loket', () => {
console.error('❌ [loketStore] Error fetching loket config:', error);
apiError.value = error.message;
// FALLBACK: If API fails, populate with dummy Reguler data for Dev/Offline mode
if (apiLoketData.value.length === 0) {
console.warn('⚠️ [loketStore] Using FALLBACK data due to API failure...');
const dummyLokets = Array.from({length: 6}, (_, i) => ({
id: i + 1,
namaLoket: `LOKET ${i + 1} REG (Mock)`,
kodeLoket: `L${i+1}`,
kuota: 100,
pelayanan: ['UM', 'BP', 'OB', 'AN', 'IP', 'SR', 'TH', 'MT', 'KK', 'PR'], // Mock all services
_spesialisDetail: [], // Empty detail
pembayaran: 'BPJS, Umum',
source: 'api', // Mimic API
loketAktif: true,
jenisloket: 'REGULER',
tipeloket: 'REGULER',
no: i + 1
}));
apiLoketData.value = dummyLokets;
return { success: true, message: 'Menggunakan data fallback (API Offline)', warning: true };
// FALLBACK 1: Use cached data if available
if (apiLoketData.value.length > 0) {
const cacheAge = lastSyncTimestamp.value
? Math.floor((Date.now() - new Date(lastSyncTimestamp.value).getTime()) / 60000)
: 'unknown';
console.warn(`⚠️ [loketStore] API failed, using cached data (${cacheAge} minutes old)`);
return {
success: true,
message: `API gagal, menggunakan data cache (${cacheAge} menit yang lalu)`,
warning: true,
cached: true
};
}
// FALLBACK 2: If no cache, populate with dummy Reguler data for Dev/Offline mode
console.warn('⚠️ [loketStore] No cache available, using FALLBACK dummy data...');
const dummyLokets = Array.from({length: 6}, (_, i) => ({
id: i + 1,
namaLoket: `LOKET ${i + 1} REG (Mock)`,
kodeLoket: `L${i+1}`,
kuota: 100,
pelayanan: ['UM', 'BP', 'OB', 'AN', 'IP', 'SR', 'TH', 'MT', 'KK', 'PR'], // Mock all services
_spesialisDetail: [], // Empty detail
pembayaran: ['BPJS', 'UMUM'], // Changed to array for consistency
tipeLoket: 'REGULER', // Add tipeLoket for fallback data
source: 'api', // Mimic API
loketAktif: true,
jenisloket: 'REGULER',
tipeloket: 'REGULER',
no: i + 1
}));
apiLoketData.value = dummyLokets;
return { success: true, message: 'Menggunakan data fallback (API Offline)', warning: true };
return { success: false, message: `Gagal memuat: ${error.message}` };
} finally {
isLoadingAPI.value = false;
@@ -393,7 +424,11 @@ export const useLoketStore = defineStore('loket', () => {
};
return {
// State
// State - Base refs (for persist)
apiLoketData, // Raw API data array (persisted)
localLoketData, // Raw local data array (persisted)
// State - Computed
loketData,
lokets: loketData, // Alias for backward compatibility/clarity
availableServices,
@@ -417,7 +452,11 @@ export const useLoketStore = defineStore('loket', () => {
persist: {
key: 'loket-store-state',
storage: typeof window !== 'undefined' ? localStorage : undefined,
paths: ['localLoketData'], // ONLY persist EKSEKUTIF data
paths: [
'localLoketData', // Persist EKSEKUTIF data (ID 1000+)
'apiLoketData', // Persist API data (REGULER) to minimize data loss
'lastSyncTimestamp' // Track when API data was last fetched
],
serializer: {
deserialize: JSON.parse,
serialize: JSON.stringify,
+623 -299
View File
File diff suppressed because it is too large Load diff