feat: Implement Anjungan (Kiosk) module including clinic room selection, various queue pages, patient check-in, and associated admin interfaces.

This commit is contained in:
Fanrouver
2026-02-19 08:28:00 +07:00
parent d03fa632e7
commit 0c4019c67d
17 changed files with 42 additions and 44 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export default defineNuxtConfig({
authUrl: process.env.AUTH_ORIGIN,
// authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001",
// authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001",
wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.123.135:8084/api/v1/ws',
wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.150.100:8084/api/v1/ws',
ekstrakExpertiseUrl: process.env.EKSTRAK_EXPERTISE_URL || 'http://10.10.123.218/ekstrakexpertise',
verificationApiBaseUrl: process.env.VERIFICATION_API_BASE_URL || 'http://10.10.123.140:8089/api/v1',
},
+6 -6
View File
@@ -801,7 +801,6 @@ import { useRuangStore } from '@/stores/ruangStore';
import PageHeader from '@/components/common/PageHeader.vue';
import PatientCard from '@/components/features/queue/PatientCard.vue';
import AppSnackbar from '@/components/common/AppSnackbar.vue';
import { useWebSocket } from '@/composables/useWebSocket';
import { useThermalPrint } from '@/composables/useThermalPrint';
const route = useRoute();
@@ -1506,21 +1505,22 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => {
console.log('📦 Nomor antrian:', nomorAntrian);
console.log('📦 Tipe layanan:', tipeLayanan);
// Send to all relevant clients
for (const clientId of anjunganClientIds) {
// Kirim nomor antrian + tipe layanan agar Anjungan tahu harus tampilkan di kolom mana
// Send to all relevant clients in parallel for speed
await Promise.all(anjunganClientIds.map(async (clientId) => {
const message = {
to_client: clientId,
data: {
noantrian: nomorAntrian,
tipeLayanan: tipeLayanan // "Pemeriksaan Awal" atau "Tindakan"
klinikId: klinikData.value?.kodeKlinik,
tipeLayanan: tipeLayanan,
triggerRefresh: true
},
};
console.log(`📨 Sending to ${clientId}:`, message);
const result = await sendViaPost(message);
console.log(`✅ Response from ${clientId}:`, result);
}
}));
console.log('✅ All WebSocket messages sent successfully');
} catch (error) {
+6 -6
View File
@@ -312,7 +312,6 @@ import { useMasterStore } from "@/stores/masterStore";
import { useLoketStore } from "@/stores/loketStore";
import { useClinicStore } from "@/stores/clinicStore";
import { useRuangStore } from "@/stores/ruangStore";
import { useWebSocket } from "@/composables/useWebSocket";
import PageHeader from "@/components/common/PageHeader.vue";
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
@@ -942,7 +941,7 @@ const handleConfirmReplace = () => {
pendingReplaceAction.value = null;
};
const { sendViaPost } = useWebSocket();
const { sendViaPost } = queueStore;
const broadcastUpdate = async (callData = null) => {
try {
@@ -971,12 +970,13 @@ const broadcastUpdate = async (callData = null) => {
};
}
for (const clientId of displayClientIds) {
await sendViaPost({
// Send all broadcasts in parallel for speed
await Promise.all(displayClientIds.map(clientId =>
sendViaPost({
to_client: clientId,
data: payload
});
}
})
));
} catch (error) {
console.error('❌ [AdminLoket] Error broadcasting update:', error);
}
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="header-left">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="28"
+1 -2
View File
@@ -3,7 +3,7 @@
<div class="page-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="40"
@@ -708,7 +708,6 @@ import { useAnjunganStore } from "@/stores/anjunganStore";
import { useClinicStore } from "@/stores/clinicStore";
import { useQueueStore } from "@/stores/queueStore";
import { useDoctorStore } from "@/stores/doctorStore";
import { useWebSocket } from "@/composables/useWebSocket";
import { useThermalPrint } from "@/composables/useThermalPrint";
definePageMeta({
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="selection-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="40"
+2 -3
View File
@@ -6,7 +6,7 @@
<div class="header-left">
<div class="logo-circle">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="64"
@@ -168,7 +168,6 @@ import { useQueueStore } from '@/stores/queueStore'
import { useMasterStore } from '@/stores/masterStore'
import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore'
import { useRoute, useRuntimeConfig } from '#app'
import { useWebSocket } from '@/composables/useWebSocket'
definePageMeta({
layout: false,
@@ -498,7 +497,7 @@ const uniqueSessionSuffix = ref(process.client ? Math.random().toString(36).subs
const anjunganClientId = computed(() => {
if (!screenId.value) return ''
return `anjungan-masuk-${screenId.value}-${uniqueSessionSuffix.value}`
return `anjungan-masuk-${screenId.value}`
})
// let wsInstance = null
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="selection-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="40"
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="tv-header-left">
<div class="hospital-logo">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="64"
@@ -5,7 +5,7 @@
<div class="header-left">
<div class="logo-circle">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="64"
@@ -148,7 +148,6 @@ import { useMasterStore } from '@/stores/masterStore'
import { useClinicStore } from '@/stores/clinicStore'
import { useRuangStore } from '@/stores/ruangStore'
import { useRoute } from '#app'
import { useWebSocket } from '@/composables/useWebSocket'
definePageMeta({
layout: false,
@@ -502,8 +501,8 @@ const anjunganClientId = computed(() => {
}
// Priority 3: Fallback to client ID without screen number (broadcast)
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-${uniqueSessionSuffix.value}`
console.log('🆔 Using default client ID (tab-specific):', clientId)
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}`
console.log('🆔 Using default client ID (broadcast):', clientId)
return clientId
})
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="selection-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="40"
+2 -3
View File
@@ -6,7 +6,7 @@
<div class="header-left">
<div class="logo-circle">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="64"
@@ -197,7 +197,6 @@ import { useLoketStore } from '@/stores/loketStore'
import { useMasterStore } from '@/stores/masterStore'
import { useClinicStore } from '@/stores/clinicStore'
import { useRoute, useRuntimeConfig } from '#app'
import { useWebSocket } from '@/composables/useWebSocket'
definePageMeta({
layout: false,
@@ -861,7 +860,7 @@ const config = useRuntimeConfig()
const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.123.135:8084/api/v1/ws'
const anjunganClientId = computed(() => {
if (!loketId.value) return ''
return `anjungan-loket-${loketId.value}-${uniqueSessionSuffix.value}`
return `anjungan-loket-${loketId.value}`
})
const fetchAllData = async () => {
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="selection-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="40"
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="header-left">
<div class="logo-circle">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
src="/RSSA logo 1.png"
alt="RSUD Logo"
class="header-logo"
width="64"
+1 -2
View File
@@ -2,7 +2,7 @@
<div>
<!-- Page Header Component -->
<PageHeader
logo="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
logo="/RSSA logo 1.png"
title="Check-In Pasien"
subtitle="Scan QR Code atau Input Manual untuk Check-In"
:show-add-button="true"
@@ -1669,7 +1669,6 @@
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
import { useQueueStore } from "@/stores/queueStore";
import { useMasterStore } from "@/stores/masterStore";
import { useWebSocket } from "@/composables/useWebSocket";
import { useThermalPrint } from "@/composables/useThermalPrint";
import PageHeader from "@/components/common/PageHeader.vue";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

+13 -10
View File
@@ -86,12 +86,12 @@ export const useQueueStore = defineStore('queue', () => {
isLoadingPatients.value = true;
apiPatientsError.value = null;
// THROTTLE: Check if we recently fetched (within last 5 seconds)
// THROTTLE: Check if we recently fetched (within last 2 seconds)
const now = Date.now();
const lastFetch = lastFetchTime.value[`clinic-${kodeKlinik}`] || 0;
const timeSinceLastFetch = now - lastFetch;
if (timeSinceLastFetch < 5000) {
if (timeSinceLastFetch < 2000) {
console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
isLoadingPatients.value = false;
return { success: true, message: 'Using cache' };
@@ -280,19 +280,20 @@ export const useQueueStore = defineStore('queue', () => {
// ============================================
const isWsConnected = ref(false);
const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`);
const lastGlobalCall = ref(null);
const onWsMessage = (data) => {
const messageData = data?.data || data;
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
const isTrigger = messageData?.triggerRefresh || targetLoketId || targetKlinikId;
if (!isTrigger) {
// console.log('📨 [queueStore] Global WS Message received (No trigger, skipping refresh)');
return;
console.log('📨 [queueStore] Global WS Message received:', data);
// Handle Call Events (cross-device sync for "Sedang Dipanggil" popup)
if (messageData?.callEvent) {
console.log('📞 [queueStore] Call event received:', messageData.callEvent);
lastGlobalCall.value = messageData.callEvent;
}
console.log('📨 [queueStore] Global WS Trigger Message received:', data);
// TRIGGER STRATEGIC REFRESHES
let refreshedSomething = false;
@@ -355,6 +356,7 @@ export const useQueueStore = defineStore('queue', () => {
const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({
url: wsBaseUrl,
clientId: wsClientId,
fallbackPostUrl: '/stats-api/ws',
onOpen: () => {
console.log('✅ [queueStore] WebSocket connected');
isWsConnected.value = true;
@@ -617,12 +619,12 @@ export const useQueueStore = defineStore('queue', () => {
isLoadingPatients.value = true;
apiPatientsError.value = null;
// THROTTLE: Check if we recently fetched (within last 5 seconds)
// THROTTLE: Check if we recently fetched (within last 2 seconds)
const now = Date.now();
const lastFetch = lastFetchTime.value[loketId] || 0;
const timeSinceLastFetch = now - lastFetch;
if (timeSinceLastFetch < 5000 && apiPatientsPerLoket.value[loketId]) {
if (timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) {
console.log(`⏭️ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
isLoadingPatients.value = false;
return {
@@ -3341,6 +3343,7 @@ export const useQueueStore = defineStore('queue', () => {
disconnectWebSocket,
isWsConnected,
sendViaPost,
lastGlobalCall,
registerInterest,
unregisterInterest,
activeLoketInterest,