feat: implement proxy routes for APIs, add Pinia stores for loket, clinic, and queue management, and create new pages for admin and anjungan dashboard.

This commit is contained in:
Fanrouver
2026-06-03 09:18:48 +07:00
parent 047b39064d
commit 0928e78bec
15 changed files with 46 additions and 24 deletions
@@ -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}/`
@@ -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}/`
@@ -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}/`
+2 -2
View File
@@ -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
+3
View File
@@ -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
+2 -2
View File
@@ -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' },
+3 -2
View File
@@ -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`,
{
+9 -1
View File
@@ -699,7 +699,11 @@
</v-snackbar>
</div>
</div>
<div v-if="!anjunganData" class="not-found">
<div v-else-if="anjunganStore.isLoading" class="not-found">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
<p class="mt-4">Memuat Konfigurasi Anjungan...</p>
</div>
<div v-else class="not-found">
<v-icon size="64" color="grey">mdi-alert-circle-outline</v-icon>
<p>Anjungan tidak ditemukan</p>
<v-btn color="primary" variant="flat" @click="backToList">Kembali</v-btn>
@@ -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();
+9 -1
View File
@@ -746,7 +746,11 @@
</v-snackbar>
</div>
</div>
<div v-if="!anjunganData" class="not-found">
<div v-else-if="anjunganStore.isLoading" class="not-found">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
<p class="mt-4">Memuat Konfigurasi Anjungan...</p>
</div>
<div v-else class="not-found">
<v-icon size="64" color="grey">mdi-alert-circle-outline</v-icon>
<p>Anjungan tidak ditemukan</p>
<v-btn color="primary" variant="flat" @click="backToList">Kembali</v-btn>
@@ -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();
+1 -1
View File
@@ -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()
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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) {
+3 -2
View File
@@ -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}`);
}
+4 -4
View File
@@ -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,