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)"
>
@@ -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']
+ }
+});