feat: Implement Anjungan (Kiosk) module including clinic room selection, various queue pages, patient check-in, and associated admin interfaces.
This commit is contained in:
+1
-1
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user