From 0928e78bec48fd385b9a314480eef72f59b6aba2 Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Wed, 3 Jun 2026 09:18:48 +0700 Subject: [PATCH] feat: implement proxy routes for APIs, add Pinia stores for loket, clinic, and queue management, and create new pages for admin and anjungan dashboard. --- .../routes => backup/proxy-routes}/klinik-api/[...].ts | 4 ++-- .../routes => backup/proxy-routes}/stats-api/[...].ts | 4 ++-- .../routes => backup/proxy-routes}/visit-api/[...].ts | 4 ++-- composables/useVisitAPI.ts | 4 ++-- docker-compose.yml | 3 +++ pages/AdminKlinikRuang/[kodeKlinik].vue | 4 ++-- pages/AdminLoket/[id].vue | 5 +++-- pages/Anjungan/Anjungan/[id].vue | 10 +++++++++- pages/Anjungan/AnjunganCopy/[id].vue | 10 +++++++++- pages/Anjungan/AntreanMasuk/[id].vue | 2 +- pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue | 2 +- pages/Anjungan/AntrianLoket/[id].vue | 2 +- stores/clinicStore.js | 3 ++- stores/loketStore.js | 5 +++-- stores/queueStore.js | 8 ++++---- 15 files changed, 46 insertions(+), 24 deletions(-) rename {server/routes => backup/proxy-routes}/klinik-api/[...].ts (92%) rename {server/routes => backup/proxy-routes}/stats-api/[...].ts (92%) rename {server/routes => backup/proxy-routes}/visit-api/[...].ts (92%) diff --git a/server/routes/klinik-api/[...].ts b/backup/proxy-routes/klinik-api/[...].ts similarity index 92% rename from server/routes/klinik-api/[...].ts rename to backup/proxy-routes/klinik-api/[...].ts index 4a7d3c0..34576c2 100644 --- a/server/routes/klinik-api/[...].ts +++ b/backup/proxy-routes/klinik-api/[...].ts @@ -1,7 +1,7 @@ import { defineEventHandler, proxyRequest } from 'h3' export default defineEventHandler(async (event) => { - const config = useRuntimeConfig() + const config = useRuntimeConfig(event) const targetBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1' // Extract path suffix after /klinik-api @@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => { } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + const proxyOrigin = config.proxyClientOrigin as string headers.origin = proxyOrigin headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` diff --git a/server/routes/stats-api/[...].ts b/backup/proxy-routes/stats-api/[...].ts similarity index 92% rename from server/routes/stats-api/[...].ts rename to backup/proxy-routes/stats-api/[...].ts index 2462c9e..bba4e21 100644 --- a/server/routes/stats-api/[...].ts +++ b/backup/proxy-routes/stats-api/[...].ts @@ -1,7 +1,7 @@ import { defineEventHandler, proxyRequest } from 'h3' export default defineEventHandler(async (event) => { - const config = useRuntimeConfig() + const config = useRuntimeConfig(event) const targetBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1' // Extract path suffix after /stats-api @@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => { } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + const proxyOrigin = config.proxyClientOrigin as string headers.origin = proxyOrigin headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` diff --git a/server/routes/visit-api/[...].ts b/backup/proxy-routes/visit-api/[...].ts similarity index 92% rename from server/routes/visit-api/[...].ts rename to backup/proxy-routes/visit-api/[...].ts index 4129664..71946da 100644 --- a/server/routes/visit-api/[...].ts +++ b/backup/proxy-routes/visit-api/[...].ts @@ -1,7 +1,7 @@ import { defineEventHandler, proxyRequest } from 'h3' export default defineEventHandler(async (event) => { - const config = useRuntimeConfig() + const config = useRuntimeConfig(event) const targetBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1' // Extract path suffix after /visit-api @@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => { } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + const proxyOrigin = config.proxyClientOrigin as string headers.origin = proxyOrigin headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` diff --git a/composables/useVisitAPI.ts b/composables/useVisitAPI.ts index 0664e50..de4b941 100644 --- a/composables/useVisitAPI.ts +++ b/composables/useVisitAPI.ts @@ -18,8 +18,8 @@ export interface VisitStats { export const useVisitAPI = () => { const config = useRuntimeConfig(); - // We use the configured proxy path to avoid CORS issues - const statsURL = '/stats-api/visit/stats'; + // We use the configured external API URL directly + const statsURL = `${config.public.externalApiBaseUrl}/visit/stats`; /** * Fetch visit statistics diff --git a/docker-compose.yml b/docker-compose.yml index a83d52f..db43945 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,9 @@ services: - NUXT_PUBLIC_WS_API_URL=${WS_API_URL} - NUXT_SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS} - NUXT_OAUTH_STATE_DURATION_MINUTES=${OAUTH_STATE_DURATION_MINUTES} + - NUXT_PROXY_CLIENT_ORIGIN=${PROXY_CLIENT_ORIGIN} + - NUXT_PROXY_TARGET_HOST_FALLBACK=${PROXY_TARGET_HOST_FALLBACK} + - NUXT_PROXY_TARGET_HOST_KLINIK_FALLBACK=${PROXY_TARGET_HOST_KLINIK_FALLBACK} volumes: # Mount local data directory to persist the sqlite users database - ./data:/app/data diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index bafc2d5..272d0a3 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -809,6 +809,7 @@ const queueStore = useQueueStore(); const masterStore = useMasterStore(); const clinicStore = useClinicStore(); const ruangStore = useRuangStore(); +const config = useRuntimeConfig(); const kodeKlinik = computed(() => { const kode = route.params.kodeKlinik; @@ -931,7 +932,6 @@ const filterOptionsList = { }; // WebSocket configuration -const config = useRuntimeConfig(); // Use a DETERMINISTIC client ID so that other pages can broadcast directly to this admin. // Format: admin-klinik-ruang-{kodeKlinik} — stable and targetable from any device. @@ -1717,7 +1717,7 @@ const handleCallPatientByTipe = async (ruang, tipeLayanan) => { visit_code: patient.barcode || patient.visitCode, visit_status_id: [visitStatusId] }; - const visitApiBase = '/visit-api'; + const visitApiBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'; const apiResponse = await fetch(`${visitApiBase}/visit/status/finish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index 04c218a..1a8ae03 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -339,6 +339,7 @@ const loketStore = useLoketStore(); const clinicStore = useClinicStore(); const ruangStore = useRuangStore(); const { printTicketFromPatient } = useThermalPrint(); +const config = useRuntimeConfig(); // Broadcast Channel let broadcastChannel = null; @@ -503,7 +504,7 @@ const apiQuota = ref(null); // Fetch latest quota data specifically for this loket const fetchQuotaFromAPI = async () => { try { - const apiBase = '/klinik-api'; + const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; const response = await fetch( `${apiBase}/klinik/loket`, ); @@ -1232,7 +1233,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => { console.log("📤 Sending visit ticket to API:", visitTicketBody); - const visitApiBase = '/visit-api'; + const visitApiBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'; const visitResponse = await fetch( `${visitApiBase}/visit/ticket/klinik`, { diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index f8f2199..f3fda1a 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -699,7 +699,11 @@ -
+
+ +

Memuat Konfigurasi Anjungan...

+
+
mdi-alert-circle-outline

Anjungan tidak ditemukan

Kembali @@ -791,6 +795,10 @@ onMounted(async () => { // Wait for stores to be hydrated and ready await nextTick(); + if (anjunganItems.value.length === 0) { + await anjunganStore.fetchAnjungan(); + } + // Initial fetch await fetchAllData(); diff --git a/pages/Anjungan/AnjunganCopy/[id].vue b/pages/Anjungan/AnjunganCopy/[id].vue index 890bcb3..a4035a6 100644 --- a/pages/Anjungan/AnjunganCopy/[id].vue +++ b/pages/Anjungan/AnjunganCopy/[id].vue @@ -746,7 +746,11 @@
-
+
+ +

Memuat Konfigurasi Anjungan...

+
+
mdi-alert-circle-outline

Anjungan tidak ditemukan

Kembali @@ -842,6 +846,10 @@ onMounted(async () => { // Wait for stores to be hydrated and ready await nextTick(); + if (anjunganItems.value.length === 0) { + await anjunganStore.fetchAnjungan(); + } + // Initial fetch await fetchAllData(); diff --git a/pages/Anjungan/AntreanMasuk/[id].vue b/pages/Anjungan/AntreanMasuk/[id].vue index 416510b..9797acc 100644 --- a/pages/Anjungan/AntreanMasuk/[id].vue +++ b/pages/Anjungan/AntreanMasuk/[id].vue @@ -515,7 +515,7 @@ const initWebSocketLocal = () => { wsInstance = useWebSocket({ url: wsBaseUrl, clientId: anjunganClientId.value, - fallbackPostUrl: '/stats-api/ws', + fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, onMessage: (data) => { console.log('📨 WebSocket message received:', data) fetchAllData() diff --git a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue index 029a73e..6e11a39 100644 --- a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue +++ b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue @@ -591,7 +591,7 @@ const initWebSocketLocal = () => { wsInstance = useWebSocket({ url: wsBaseUrl, clientId: anjunganClientId.value, - fallbackPostUrl: '/stats-api/ws', + fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, onMessage: (data) => { console.log('📨 WebSocket raw message received:', data) let messageData = data diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index 4394183..ce8be35 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -941,7 +941,7 @@ const initWebSocketLocal = () => { wsInstance = useWebSocket({ url: wsBaseUrl, clientId: anjunganClientId.value, - fallbackPostUrl: '/stats-api/ws', + fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, onMessage: (data) => { console.log('📨 WebSocket message received:', data) const messageData = data?.data || data diff --git a/stores/clinicStore.js b/stores/clinicStore.js index 077538b..590dbe2 100644 --- a/stores/clinicStore.js +++ b/stores/clinicStore.js @@ -3,6 +3,7 @@ import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useClinicStore = defineStore('clinic', () => { + const config = useRuntimeConfig(); // Data clinics - Single source of truth untuk semua data klinik // Includes basic info (name, kode, icon, doctors, shifts) + master config (totalQuota, jamShiftPerHari, jadwalKlinik, tanggalTutup) @@ -744,7 +745,7 @@ export const useClinicStore = defineStore('clinic', () => { let rawData; try { - rawData = await $fetch('/klinik-api/klinik/reguler'); + rawData = await $fetch(`${config.public.verificationApiBaseUrl}/klinik/reguler`); } catch (error) { // Handle Rate Limiting with exponential backoff if (error.response?.status === 429 && retryCount < 3) { diff --git a/stores/loketStore.js b/stores/loketStore.js index 7ff81a1..5ea6e2d 100644 --- a/stores/loketStore.js +++ b/stores/loketStore.js @@ -68,6 +68,7 @@ const getPelayananContohDariAnjungan = (anjunganItems) => { export const useLoketStore = defineStore('loket', () => { const clinicStore = useClinicStore(); const anjunganStore = useAnjunganStore(); + const config = useRuntimeConfig(); // ============================================ // STATE - SEPARATED DATA SOURCES @@ -289,7 +290,7 @@ export const useLoketStore = defineStore('loket', () => { let rawData; try { - rawData = await $fetch('/klinik-api/klinik/loket'); + rawData = await $fetch(`${config.public.verificationApiBaseUrl}/klinik/loket`); } catch (error) { if (error.response?.status === 429 && retryCount < 3) { const delay = (retryCount + 1) * 1500; @@ -428,7 +429,7 @@ export const useLoketStore = defineStore('loket', () => { let rawData; try { - rawData = await $fetch(`/klinik-api/klinik/loket/${loketId}`); + rawData = await $fetch(`${config.public.verificationApiBaseUrl}/klinik/loket/${loketId}`); } catch (error) { throw new Error(`HTTP error! status: ${error.response?.status || error.message}`); } diff --git a/stores/queueStore.js b/stores/queueStore.js index 8b4bdc3..9eb3c89 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -100,7 +100,7 @@ export const useQueueStore = defineStore('queue', () => { throw new Error(`Klinik ID tidak ditemukan untuk kode: ${kodeKlinik}`); } - const url = `/visit-api/visit?klinik_id=${clinic.id}&limit=500`; + const url = `${config.public.externalApiBaseUrl}/visit?klinik_id=${clinic.id}&limit=500`; console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`); const response = await fetch(url); @@ -429,7 +429,7 @@ export const useQueueStore = defineStore('queue', () => { const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({ url: wsBaseUrl, clientId: wsClientId, - fallbackPostUrl: '/stats-api/ws', + fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, reconnectInterval: 2000, // 2 seconds between reconnect attempts maxReconnectAttempts: 9999, // Effectively infinite — never give up on remote machines onOpen: () => { @@ -2789,7 +2789,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { case "terlambat": // Send API call to update status to 33 (TR PEMERIKSAAN) try { - const visitApiBase = '/visit-api'; + const visitApiBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'; const apiUrl = `${visitApiBase}/visit/status/finish`; const requestBody = { patient_visit_healthcare_service_id: patient.healthcareServiceId, @@ -2814,7 +2814,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { case "pending": // Send API call to update status to 32 (PE PEMERIKSAAN) try { - const visitApiBase = '/visit-api'; + const visitApiBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'; const apiUrl = `${visitApiBase}/visit/status/finish`; const requestBody = { patient_visit_healthcare_service_id: patient.healthcareServiceId,