update logika penarikan API dan tampilan anjungan

This commit is contained in:
bagus-arie05
2026-01-22 10:36:28 +07:00
parent 2ccb378672
commit e900c5a1c2
8 changed files with 243 additions and 75 deletions

No files matched your search

+1 -1
View File
@@ -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;
+14 -61
View File
@@ -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 {
+4 -4
View File
@@ -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;
}
+2 -2
View File
@@ -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;
}
+2 -4
View File
@@ -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;
}
+58
View File
@@ -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
View File
@@ -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'],
},
});
+127
View File
@@ -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']
}
});