update logika penarikan API dan tampilan anjungan
This commit is contained in:
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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)'
|
||||
"
|
||||
></v-icon>
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<div class="anjungan-klinik-preview">
|
||||
<div class="klinik-count">
|
||||
<v-icon size="16" color="secondary">mdi-hospital-box</v-icon>
|
||||
<v-icon size="16" color="primary-600">mdi-hospital-box</v-icon>
|
||||
<span>{{ anj.klinik.length }} Klinik</span>
|
||||
</div>
|
||||
<div class="klinik-tags">
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,8 @@
|
||||
@click="navigateToLoket(loket.id)"
|
||||
>
|
||||
<div class="loket-card-header">
|
||||
<v-icon size="32" color="primary-600">mdi-view-dashboard</v-icon>
|
||||
<div class="loket-info">
|
||||
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
|
||||
<p class="loket-details">No. {{ numberToLetter(loket.no) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
+35
-3
@@ -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'],
|
||||
},
|
||||
});
|
||||
@@ -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']
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user