update queuestore dari API
This commit is contained in:
No files matched your search
+179
-44
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}` : '';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load diff
Reference in New Issue
Block a user