From e900c5a1c2cbb4c53eaf972d22c81be9eb4ba091 Mon Sep 17 00:00:00 2001 From: bagus-arie05 Date: Thu, 22 Jan 2026 10:36:28 +0700 Subject: [PATCH] update logika penarikan API dan tampilan anjungan --- pages/AdminKlinikRuang/[kodeKlinik].vue | 2 +- pages/Anjungan/Anjungan/[id].vue | 75 +++--------- pages/Anjungan/Anjungan/index.vue | 8 +- pages/Anjungan/AntrianKlinikRuang/index.vue | 4 +- pages/Anjungan/AntrianLoket/index.vue | 6 +- plugins/scheduler.client.ts | 58 +++++++++ stores/clinicStore.js | 38 +++++- stores/doctorStore.js | 127 ++++++++++++++++++++ 8 files changed, 243 insertions(+), 75 deletions(-) create mode 100644 plugins/scheduler.client.ts create mode 100644 stores/doctorStore.js diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index 9add41f..98e64f8 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -2020,7 +2020,7 @@ onMounted(() => { } .action-grid .v-btn:disabled { - background-color: var(--color-neutral-700) !important; + background-color: var(--color-neutral-600) !important; color: var(--color-neutral-100) !important; opacity: 0.8 !important; box-shadow: none !important; diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 6b89d3a..3b8b214 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -49,7 +49,7 @@ size="14" :color=" clinic.available - ? 'var(--color-primary-600)' + ? 'var(--color-neutral-100)' : 'var(--color-neutral-600)' " > @@ -63,7 +63,7 @@ size="12" :color=" clinic.available - ? 'var(--color-success-600)' + ? 'var(--color-neutral-100)' : 'var(--color-neutral-600)' " > @@ -78,7 +78,7 @@ size="28" :color=" clinic.available - ? 'var(--color-success-600)' + ? 'var(--color-primary-600)' : 'var(--color-neutral-100)' " > @@ -677,6 +677,7 @@ import { storeToRefs } from "pinia"; import { useAnjunganStore } from "@/stores/anjunganStore"; import { useClinicStore } from "@/stores/clinicStore"; import { useQueueStore } from "@/stores/queueStore"; +import { useDoctorStore } from "@/stores/doctorStore"; import { useThermalPrint } from "@/composables/useThermalPrint"; definePageMeta({ @@ -687,11 +688,13 @@ const route = useRoute(); const anjunganStore = useAnjunganStore(); const clinicStore = useClinicStore(); const queueStore = useQueueStore(); +const doctorStore = useDoctorStore(); const { printTicketFromPatient, isPrinting } = useThermalPrint(); // Gunakan storeToRefs untuk memastikan reaktivitas yang stabil terutama saat cold start/refresh const { anjunganItems } = storeToRefs(anjunganStore); const { getAllClinics } = storeToRefs(clinicStore); +const { doctorsByKlinikId, loadingDoctors } = storeToRefs(doctorStore); // Validasi bahwa queueStore ter-load dengan benar onMounted(() => { @@ -719,61 +722,11 @@ const anjunganData = computed(() => { return list.find((a) => Number(a.id) === Number(idValue)) || null; }); -const doctorsByKlinikId = ref({}); -const loadingDoctors = ref({}); +// doctorsByKlinikId and loadingDoctors are now from doctorStore -// Load doctors dari API berdasarkan idklinik +// Load doctors dari store const loadDoctorsForClinic = async (clinic) => { - if (!clinic || !clinic.id) return; - - const idklinik = clinic.id; - - // cek apakah sudah ada data atau sedang loading - if (doctorsByKlinikId.value[idklinik] || loadingDoctors.value[idklinik]) { - return; - } - - loadingDoctors.value[idklinik] = true; - - try { - console.log(`🔄 Fetching doctors for klinik ID: ${idklinik} (${clinic.name})`); - const data = await $fetch( - `http://10.10.150.131:8089/api/v1/dokter/${idklinik}` - ); - - console.log(`📦 Raw doctor data for ${idklinik}:`, data); - - // Handle different response formats - let doctorList = []; - - if (Array.isArray(data)) { - doctorList = data; - } else if (data && typeof data === 'object') { - if (Array.isArray(data.data)) { - doctorList = data.data; - } else if (Array.isArray(data.result)) { - doctorList = data.result; - } - } - - // Extract doctor names - const doctorNames = doctorList - .map((d) => d.nama_dokter || d.nama_lengkap || d.Nama_dokter || d.name || d.nama) - .filter(Boolean); - - doctorsByKlinikId.value[idklinik] = doctorNames; - - console.log( - `✅ Loaded ${doctorNames.length} doctors untuk klinik ${clinic.name} (ID: ${idklinik}):`, - doctorNames - ); - } catch (error) { - console.error(`❌ Gagal mengambil dokter untuk klinik ID ${idklinik}:`, error); - // set empty array agar tidak retry - doctorsByKlinikId.value[idklinik] = []; - } finally { - loadingDoctors.value[idklinik] = false; - } + await doctorStore.fetchDoctorsForClinic(clinic); }; const prefetchDoctorsForCurrentAnjungan = async () => { @@ -1409,8 +1362,8 @@ watch(lastRegisteredPatient, (newVal) => { } .clinic-available { - border-color: var(--color-success-600); - background: var(--color-success-100); + border-color: var(--color-primary-600); + background: var(--color-neutral-200); } .clinic-closed { @@ -1817,11 +1770,11 @@ watch(lastRegisteredPatient, (newVal) => { padding: 20px; background: linear-gradient( 135deg, - var(--color-success-100) 0%, - var(--color-success-200) 100% + var(--color-neutral-100) 0%, + var(--color-neutral-200) 100% ); border-radius: 12px; - border: 2px solid var(--color-success-400); + border: 2px solid var(--color-primary-400); margin-bottom: 12px; &.quota-empty { diff --git a/pages/Anjungan/Anjungan/index.vue b/pages/Anjungan/Anjungan/index.vue index a5bf4fa..a198d3c 100644 --- a/pages/Anjungan/Anjungan/index.vue +++ b/pages/Anjungan/Anjungan/index.vue @@ -33,7 +33,7 @@
- mdi-hospital-box + mdi-hospital-box {{ anj.klinik.length }} Klinik
@@ -277,7 +277,7 @@ const navigateToSettings = () => { gap: 6px; font-size: 14px; font-weight: 600; - color: var(--color-secondary-600); + color: var(--color-primary-600); margin-bottom: 12px; } @@ -288,8 +288,8 @@ const navigateToSettings = () => { } .chip-preview { - background-color: var(--color-secondary-300) !important; - color: var(--color-secondary-700) !important; + background-color: var(--color-primary-600) !important; + color: var(--color-neutral-100) !important; font-weight: 500; font-size: 12px; } diff --git a/pages/Anjungan/AntrianKlinikRuang/index.vue b/pages/Anjungan/AntrianKlinikRuang/index.vue index 5f58cb9..95e7a8a 100644 --- a/pages/Anjungan/AntrianKlinikRuang/index.vue +++ b/pages/Anjungan/AntrianKlinikRuang/index.vue @@ -315,8 +315,8 @@ const navigateToSettings = () => { } .chip-preview { - background-color: var(--color-primary-300) !important; - color: var(--color-primary-700) !important; + background-color: var(--color-primary-600) !important; + color: var(--color-neutral-100) !important; font-weight: 500; font-size: 12px; } diff --git a/pages/Anjungan/AntrianLoket/index.vue b/pages/Anjungan/AntrianLoket/index.vue index bd5325d..b84bc43 100644 --- a/pages/Anjungan/AntrianLoket/index.vue +++ b/pages/Anjungan/AntrianLoket/index.vue @@ -24,10 +24,8 @@ @click="navigateToLoket(loket.id)" >
- mdi-view-dashboard

{{ loket.namaLoket }}

-

No. {{ numberToLetter(loket.no) }}

@@ -297,8 +295,8 @@ const getKlinikName = (kode) => { } .chip-preview { - background-color: var(--color-primary-300) !important; - color: var(--color-primary-700) !important; + background-color: var(--color-primary-600) !important; + color: var(--color-neutral-100) !important; font-weight: 500; font-size: 12px; } diff --git a/plugins/scheduler.client.ts b/plugins/scheduler.client.ts new file mode 100644 index 0000000..5089e9a --- /dev/null +++ b/plugins/scheduler.client.ts @@ -0,0 +1,58 @@ +// plugins/scheduler.client.ts +import { useClinicStore } from '@/stores/clinicStore'; +import { useDoctorStore } from '@/stores/doctorStore'; + +export default defineNuxtPlugin((nuxtApp) => { + // We only want this to run on the client side (kiosks) + if (process.server) return; + + const CHECK_INTERVAL = 60 * 1000; // Check every minute + let isSyncing = false; + + const checkSync = async () => { + if (isSyncing) return; + + try { + const clinicStore = useClinicStore(); + const doctorStore = useDoctorStore(); + + const now = new Date(); + + // We target 2:00 AM. + // If it's between 2:00 and 2:05, and store says sync is needed, we trigger it. + // This window prevents missing it if the kiosk is slightly off or interval skips. + const isSyncWindow = now.getHours() === 2 && now.getMinutes() < 5; + + const clinicSyncNeeded = clinicStore.isSyncNeeded(); + const doctorSyncNeeded = doctorStore.isSyncNeeded(); + + if (isSyncWindow && (clinicSyncNeeded || doctorSyncNeeded)) { + console.log('⏰ [scheduler] 2 AM Sync window detected. Triggering sync...'); + isSyncing = true; + + // 1. Sync Clinics + console.log('🔄 [scheduler] Syncing clinics...'); + await clinicStore.fetchRegulerClinics(true); + + // 2. Sync Doctors after clinics are ready + console.log('🔄 [scheduler] Syncing doctors...'); + const allClinics = clinicStore.clinics; + await doctorStore.syncAllDoctors(allClinics); + + console.log('✅ [scheduler] Daily 2 AM sync completed successfully.'); + } + } catch (error) { + console.error('❌ [scheduler] Error during scheduled sync:', error); + } finally { + isSyncing = false; + } + }; + + // Run initial check on site load + setTimeout(checkSync, 5000); // Wait 5s for stores to hydrate + + // Set interval for periodic check + setInterval(checkSync, CHECK_INTERVAL); + + console.log('🕒 [scheduler] 2 AM Sync Scheduler initialized.'); +}); diff --git a/stores/clinicStore.js b/stores/clinicStore.js index 5850ede..4e3248f 100644 --- a/stores/clinicStore.js +++ b/stores/clinicStore.js @@ -593,6 +593,34 @@ export const useClinicStore = defineStore('clinic', () => { const isLoadingAPI = ref(false); const apiError = ref(null); + const lastSyncTimestamp = ref(null); + + /** + * Check if sync is needed based on 2 AM schedule + */ + const isSyncNeeded = () => { + if (!lastSyncTimestamp.value) return true; + + const now = new Date(); + const lastSync = new Date(lastSyncTimestamp.value); + + // Create a 2 AM today date + const today2AM = new Date(now); + today2AM.setHours(2, 0, 0, 0); + + // If last sync was before 2 AM today, and it's already past 2 AM today, we need to sync + if (lastSync < today2AM && now >= today2AM) { + return true; + } + + // If last sync was on a different day and it's before 2 AM, but more than 24h passed + const diffInHours = (now - lastSync) / (1000 * 60 * 60); + if (diffInHours > 24) { + return true; + } + + return false; + }; // Helper: Convert day name from API to Indonesian const mapDayToIndonesian = (day) => { @@ -732,9 +760,9 @@ export const useClinicStore = defineStore('clinic', () => { return activeFetchPromise; } - // Guard: Skip if data already exists and not forced + // Guard: Skip if data already exists and not forced, AND sync not needed const hasRegulerClinics = clinics.value.some(c => c.jenisLayanan === 'Reguler'); - if (hasRegulerClinics && !force && retryCount === 0) { + if (hasRegulerClinics && !force && retryCount === 0 && !isSyncNeeded()) { return { success: true, message: 'Data sudah tersedia' }; } @@ -817,6 +845,8 @@ export const useClinicStore = defineStore('clinic', () => { return a.id - b.id; }); + lastSyncTimestamp.value = new Date().toISOString(); + return { success: true, message: `${regulerClinics.length} klinik reguler berhasil dimuat` }; } catch (error) { @@ -910,6 +940,8 @@ export const useClinicStore = defineStore('clinic', () => { clinics, isLoadingAPI, apiError, + lastSyncTimestamp, + isSyncNeeded, getAllClinics, getClinicsByJenisLayanan, getClinicByName, @@ -922,6 +954,6 @@ export const useClinicStore = defineStore('clinic', () => { persist: { key: 'clinic-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['clinics'], + paths: ['clinics', 'lastSyncTimestamp'], }, }); diff --git a/stores/doctorStore.js b/stores/doctorStore.js new file mode 100644 index 0000000..b66ef0c --- /dev/null +++ b/stores/doctorStore.js @@ -0,0 +1,127 @@ +// stores/doctorStore.js +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const useDoctorStore = defineStore('doctor', () => { + const doctorsByKlinikId = ref({}); + const lastSyncTimestamp = ref(null); + const loadingDoctors = ref({}); + + /** + * Check if sync is needed based on 2 AM schedule + */ + const isSyncNeeded = () => { + if (!lastSyncTimestamp.value) return true; + + const now = new Date(); + const lastSync = new Date(lastSyncTimestamp.value); + + // Create a 2 AM today date + const today2AM = new Date(now); + today2AM.setHours(2, 0, 0, 0); + + // If last sync was before 2 AM today, and it's already past 2 AM today, we need to sync + if (lastSync < today2AM && now >= today2AM) { + return true; + } + + // If last sync was on a different day and it's before 2 AM, but more than 24h passed + // (This is a safety check for long-running sessions) + const diffInHours = (now - lastSync) / (1000 * 60 * 60); + if (diffInHours > 24) { + return true; + } + + return false; + }; + + /** + * Fetch doctors for a clinic from API + */ + const fetchDoctorsForClinic = async (clinic, force = false) => { + if (!clinic || !clinic.id) return; + + const idklinik = clinic.id; + + // Skip if already in cache and not forced, and sync not needed + if (!force && doctorsByKlinikId.value[idklinik] && !isSyncNeeded()) { + return doctorsByKlinikId.value[idklinik]; + } + + // Skip if already loading + if (loadingDoctors.value[idklinik]) { + return; + } + + loadingDoctors.value[idklinik] = true; + + try { + console.log(`🔄 [doctorStore] Fetching doctors for klinik ID: ${idklinik} (${clinic.name})`); + const data = await $fetch( + `http://10.10.150.131:8089/api/v1/dokter/${idklinik}` + ); + + let doctorList = []; + if (Array.isArray(data)) { + doctorList = data; + } else if (data && typeof data === 'object') { + if (Array.isArray(data.data)) { + doctorList = data.data; + } else if (Array.isArray(data.result)) { + doctorList = data.result; + } + } + + const doctorNames = doctorList + .map((d) => d.nama_dokter || d.nama_lengkap || d.Nama_dokter || d.name || d.nama) + .filter(Boolean); + + doctorsByKlinikId.value[idklinik] = doctorNames; + + // Update sync timestamp if this was a refresh + if (force) { + lastSyncTimestamp.value = new Date().toISOString(); + } + + console.log(`✅ [doctorStore] Loaded ${doctorNames.length} doctors untuk klinik ${clinic.name}`); + return doctorNames; + } catch (error) { + console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik}:`, error); + // Don't overwrite existing data on failure unless it's empty + if (!doctorsByKlinikId.value[idklinik]) { + doctorsByKlinikId.value[idklinik] = []; + } + } finally { + loadingDoctors.value[idklinik] = false; + } + }; + + /** + * Sync doctors for all provided clinics + */ + const syncAllDoctors = async (clinics) => { + console.log(`🔄 [doctorStore] Syncing all doctors for ${clinics.length} clinics...`); + + // Filter clinics that need sync (only Reguler clinics usually have dynamic doctors) + const clinicsToSync = clinics.filter(c => c.id); + + await Promise.all(clinicsToSync.map(c => fetchDoctorsForClinic(c, true))); + + lastSyncTimestamp.value = new Date().toISOString(); + console.log(`✅ [doctorStore] Global doctor sync complete at ${lastSyncTimestamp.value}`); + }; + + return { + doctorsByKlinikId, + lastSyncTimestamp, + loadingDoctors, + fetchDoctorsForClinic, + syncAllDoctors, + isSyncNeeded + }; +}, { + persist: { + key: 'doctor-store', + pick: ['doctorsByKlinikId', 'lastSyncTimestamp'] + } +});