feat: implement Grand Paviliun dashboard with specialized room monitoring, queue management, and modular components
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
// variables for Grand Paviliun (Eksekutif)
|
||||
.theme-grand-paviliun {
|
||||
--gp-primary: #3F51B5; // Indigo header
|
||||
--gp-primary-dark: #303F9F;
|
||||
--gp-bg: #F5F7FA; // Light grayish blue background
|
||||
--gp-surface: #FFFFFF;
|
||||
--gp-text-primary: #1F2937;
|
||||
--gp-text-secondary: #6B7280;
|
||||
--gp-border: #E5E7EB;
|
||||
|
||||
--gp-success: #10B981; // Selesai
|
||||
--gp-danger: #EF4444; // Pending
|
||||
--gp-warning: #F59E0B;
|
||||
--gp-info: #3B82F6;
|
||||
|
||||
// Custom colors for specific tags
|
||||
--gp-room-number: #F97316; // Orange text for RUANG 02
|
||||
|
||||
background-color: var(--gp-bg);
|
||||
min-height: 100vh;
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
.gp-text-primary { color: var(--gp-text-primary); }
|
||||
.gp-text-secondary { color: var(--gp-text-secondary); }
|
||||
.gp-font-bold { font-weight: 700; }
|
||||
.gp-font-semibold { font-weight: 600; }
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="theme-grand-paviliun gp-container">
|
||||
<!-- Header -->
|
||||
<div class="gp-header">
|
||||
<div class="header-left">
|
||||
<v-icon size="24" color="white" class="mr-3">mdi-domain</v-icon>
|
||||
<div>
|
||||
<h1 class="header-title">Klinik Admin Grand Paviliun</h1>
|
||||
<p class="header-date">{{ currentDate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<v-btn color="#F97316" variant="flat" class="text-white font-weight-bold">
|
||||
<v-icon start size="18">mdi-account-cog</v-icon>
|
||||
KELOLA PASIEN
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="gp-search-bar">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Cari pasien di semua ruang (barcode, nomor antrian, nama...)"
|
||||
density="compact"
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
bg-color="white"
|
||||
class="search-input"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="gp-main-content">
|
||||
<div class="kanban-board">
|
||||
<GpRoomColumn
|
||||
v-for="clinic in filteredClinics"
|
||||
:key="clinic.kodeKlinik"
|
||||
:clinic-name="clinic.namaKlinik"
|
||||
:current-patient="clinic.currentPatient"
|
||||
:queue-patients="clinic.queuePatients"
|
||||
:room-options="clinic.roomOptions"
|
||||
@pemeriksaan-awal="(p, r) => handlePemeriksaanAwal(p, clinic.kodeKlinik, r)"
|
||||
@panggil-pemeriksaan="(p, r) => handlePanggilPemeriksaan(p, clinic.kodeKlinik, r)"
|
||||
@action-pending="p => handleAction(p, 'pending', clinic.kodeKlinik)"
|
||||
@action-selesai="p => handleAction(p, 'selesai', clinic.kodeKlinik)"
|
||||
@process-patient="p => handleProcess(p, clinic.kodeKlinik, clinic.roomOptions?.[0]?.nomorRuang || '1')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar -->
|
||||
<GpMonitorSidebar :rooms="monitorRooms" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import GpRoomColumn from '../GrandPaviliun/GpRoomColumn.vue';
|
||||
import GpMonitorSidebar from '../GrandPaviliun/GpMonitorSidebar.vue';
|
||||
import { useGrandPaviliun } from '@/composables/useGrandPaviliun';
|
||||
import '@/assets/scss/eksekutif.scss';
|
||||
|
||||
const {
|
||||
clinics,
|
||||
monitorRooms,
|
||||
fetchEksekutifData,
|
||||
handlePemeriksaanAwal,
|
||||
handlePanggilPemeriksaan,
|
||||
processPatientAction
|
||||
} = useGrandPaviliun();
|
||||
const searchQuery = ref('');
|
||||
|
||||
const filteredClinics = computed(() => {
|
||||
if (!searchQuery.value) return clinics.value;
|
||||
const lowerSearch = searchQuery.value.toLowerCase();
|
||||
|
||||
// Filter by patient barcode, queue number, or name in the clinic columns
|
||||
return clinics.value.map(clinic => {
|
||||
const isCurrentMatch = clinic.currentPatient && (
|
||||
(clinic.currentPatient.noAntrian || '').toLowerCase().includes(lowerSearch) ||
|
||||
(clinic.currentPatient.barcode || '').toLowerCase().includes(lowerSearch) ||
|
||||
(clinic.currentPatient.name || '').toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
|
||||
const matchedQueue = clinic.queuePatients.filter(p =>
|
||||
(p.noAntrian || '').toLowerCase().includes(lowerSearch) ||
|
||||
(p.barcode || '').toLowerCase().includes(lowerSearch) ||
|
||||
(p.name || '').toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
|
||||
return {
|
||||
...clinic,
|
||||
currentPatient: isCurrentMatch ? clinic.currentPatient : null,
|
||||
queuePatients: matchedQueue
|
||||
};
|
||||
}).filter(c => c.currentPatient || c.queuePatients.length > 0);
|
||||
});
|
||||
|
||||
// Action Handlers
|
||||
const handleAction = async (patient, action, kodeKlinik) => {
|
||||
// Use the current room of the patient or fallback to '1'
|
||||
const nomorRuang = patient.nomorRuang || '1';
|
||||
await processPatientAction(patient, action, kodeKlinik, nomorRuang);
|
||||
};
|
||||
|
||||
const handleProcess = async (patient, kodeKlinik, fallbackRuang) => {
|
||||
const action = patient.status === 'pending' ? 'waiting' : 'proses';
|
||||
await processPatientAction(patient, action, kodeKlinik, fallbackRuang);
|
||||
};
|
||||
|
||||
// Date Formatting
|
||||
const currentDate = computed(() => {
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return new Date().toLocaleDateString('id-ID', options);
|
||||
});
|
||||
|
||||
// Load data when mounted
|
||||
fetchEksekutifData();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--gp-bg, #F5F7FA);
|
||||
}
|
||||
.gp-header {
|
||||
background-color: var(--gp-primary, #3F51B5);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.header-date {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
.gp-search-bar {
|
||||
padding: 12px 24px;
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
}
|
||||
.search-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
.gp-main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.kanban-board {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 16px 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="gp-current-patient">
|
||||
<div class="header-row">
|
||||
<span class="label">SEDANG DIPROSES</span>
|
||||
<div class="badges">
|
||||
<button class="action-badge" @click="$emit('action-pending')"><GpStatusBadge status="PENDING" /></button>
|
||||
<button class="action-badge ml-1" @click="$emit('action-selesai')"><GpStatusBadge status="SELESAI" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="patient-big-info">
|
||||
<div class="queue-number">{{ queueNumber || '-' }}</div>
|
||||
<div class="room-indicator" v-if="roomName">{{ roomName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-section">
|
||||
<label class="input-label">Tujukan ke ruang*</label>
|
||||
<v-select
|
||||
v-model="selectedRoom"
|
||||
:items="roomOptions"
|
||||
item-title="namaRuang"
|
||||
item-value="nomorRuang"
|
||||
placeholder="Pilih Ruang Pemeriksaan"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="room-select mb-3"
|
||||
></v-select>
|
||||
|
||||
<div class="action-buttons">
|
||||
<v-btn
|
||||
color="#3F51B5"
|
||||
variant="flat"
|
||||
class="flex-1 text-white text-none font-weight-bold"
|
||||
size="small"
|
||||
@click="$emit('pemeriksaan-awal')"
|
||||
>
|
||||
<v-icon start size="16">mdi-stethoscope</v-icon>
|
||||
Pemeriksaan Awal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="#3F51B5"
|
||||
variant="flat"
|
||||
class="flex-1 text-white text-none font-weight-bold ml-2"
|
||||
size="small"
|
||||
@click="$emit('panggil-pemeriksaan')"
|
||||
>
|
||||
<v-icon start size="16">mdi-bullhorn</v-icon>
|
||||
Panggil Pemeriksaan
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import GpStatusBadge from './GpStatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
queueNumber: { type: String, default: '' },
|
||||
roomName: { type: String, default: '' },
|
||||
roomOptions: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['pemeriksaan-awal', 'panggil-pemeriksaan', 'action-pending', 'action-selesai', 'update:selectedRoom']);
|
||||
|
||||
const selectedRoom = ref(props.roomOptions?.[0]?.nomorRuang || null);
|
||||
watch(selectedRoom, (newVal) => {
|
||||
emit('update:selectedRoom', newVal);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-current-patient {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
}
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-secondary, #6b7280);
|
||||
}
|
||||
.patient-big-info {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.queue-number {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--gp-text-primary, #1f2937);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.room-indicator {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-room-number, #F97316);
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--gp-text-secondary, #6b7280);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.room-select {
|
||||
/* vuetify default is fine */
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.action-badge {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.action-badge:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="gp-monitor-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<v-icon size="18" class="mr-2" color="#3F51B5">mdi-monitor-dashboard</v-icon>
|
||||
<span class="title">MONITOR RUANG</span>
|
||||
</div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-box box-success">
|
||||
<div class="stat-label text-success">TERSEDIA</div>
|
||||
<div class="stat-value text-success">{{ availableCount }}</div>
|
||||
</div>
|
||||
<div class="stat-box box-danger ml-2">
|
||||
<div class="stat-label text-danger">SIBUK</div>
|
||||
<div class="stat-value text-danger">{{ busyCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rooms-list">
|
||||
<GpRoomMonitorCard
|
||||
v-for="room in rooms"
|
||||
:key="room.id"
|
||||
:room-name="room.name"
|
||||
:active-patient="room.activePatient"
|
||||
:clinic-name="room.clinicName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import GpRoomMonitorCard from './GpRoomMonitorCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
rooms: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
const availableCount = computed(() => props.rooms.filter(r => !r.activePatient).length);
|
||||
const busyCount = computed(() => props.rooms.filter(r => r.activePatient).length);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-monitor-sidebar {
|
||||
width: 300px;
|
||||
background: white;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--gp-border, #e5e7eb);
|
||||
}
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-primary, #1f2937);
|
||||
}
|
||||
.stats-row {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
}
|
||||
.stat-box {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.box-success {
|
||||
background: #ECFDF5;
|
||||
border: 1px solid #D1FAE5;
|
||||
}
|
||||
.box-danger {
|
||||
background: #FFF7ED;
|
||||
border: 1px solid #FFEDD5;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.text-success { color: var(--gp-success, #10b981); }
|
||||
.text-danger { color: var(--gp-room-number, #F97316); } /* Orange */
|
||||
|
||||
.rooms-list {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="gp-queue-list-container">
|
||||
<div class="list-header">
|
||||
<span class="title">DAFTAR ANTRIAN</span>
|
||||
<span class="count-badge">{{ patients.length }}</span>
|
||||
</div>
|
||||
<div class="list-body">
|
||||
<GpQueueListItem
|
||||
v-for="patient in patients"
|
||||
:key="patient.id || patient.noAntrian"
|
||||
:queue-number="patient.noAntrian"
|
||||
:status="patient.status"
|
||||
:is-active="patient.isActive"
|
||||
@view-detail="$emit('view-detail', patient)"
|
||||
@process-patient="$emit('process-patient', patient)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GpQueueListItem from './GpQueueListItem.vue';
|
||||
|
||||
defineProps({
|
||||
patients: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
defineEmits(['view-detail', 'process-patient']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-queue-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
background: white;
|
||||
min-height: 200px;
|
||||
}
|
||||
.list-header {
|
||||
padding: 12px 16px;
|
||||
background: #EEF2F6; /* Light blue-gray from design */
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-secondary, #6b7280);
|
||||
}
|
||||
.count-badge {
|
||||
background: #D1D5DB; /* Gray */
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.list-body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div :class="['gp-queue-list-item', { active: isActive }]">
|
||||
<div class="item-left">
|
||||
<span class="queue-number">{{ queueNumber }}</span>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<GpStatusBadge v-if="status && !isActive" :status="status" />
|
||||
<v-btn v-if="!isActive" icon="mdi-play" size="x-small" variant="text" color="success" class="ml-1" @click="$emit('process-patient')" />
|
||||
<v-btn v-if="isActive" icon="mdi-eye" size="x-small" variant="text" color="#3F51B5" @click="$emit('view-detail')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GpStatusBadge from './GpStatusBadge.vue';
|
||||
|
||||
defineProps({
|
||||
queueNumber: { type: String, required: true },
|
||||
status: { type: String, default: '' },
|
||||
isActive: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
defineEmits(['view-detail', 'process-patient']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-queue-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gp-border, #e5e7eb);
|
||||
background-color: var(--gp-surface, #fff);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.gp-queue-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.gp-queue-list-item.active {
|
||||
background-color: #E3EBFF; /* Light blue highlighting active patient */
|
||||
}
|
||||
.queue-number {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-primary, #1f2937);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="gp-room-column">
|
||||
<div class="column-header">
|
||||
<v-icon size="20" class="mr-2" color="white">mdi-domain</v-icon>
|
||||
<span class="column-title">{{ clinicName.toUpperCase() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="column-content">
|
||||
<template v-if="currentPatient || queuePatients.length > 0">
|
||||
<GpCurrentPatient
|
||||
v-if="currentPatient"
|
||||
:queue-number="currentPatient.noAntrian"
|
||||
:room-name="currentPatient.roomIndicator"
|
||||
:room-options="roomOptions"
|
||||
@pemeriksaan-awal="$emit('pemeriksaan-awal', currentPatient, $event)"
|
||||
@panggil-pemeriksaan="$emit('panggil-pemeriksaan', currentPatient, $event)"
|
||||
@action-pending="$emit('action-pending', currentPatient)"
|
||||
@action-selesai="$emit('action-selesai', currentPatient)"
|
||||
/>
|
||||
|
||||
<GpQueueList
|
||||
:patients="queuePatients"
|
||||
@view-detail="p => $emit('view-detail', p)"
|
||||
@process-patient="p => $emit('process-patient', p)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="empty-column-state">
|
||||
<v-icon size="48" color="#D1D5DB" class="mb-4">mdi-account-off</v-icon>
|
||||
<div class="empty-text">TIDAK ADA PASIEN YANG DIPROSES</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GpCurrentPatient from './GpCurrentPatient.vue';
|
||||
import GpQueueList from './GpQueueList.vue';
|
||||
|
||||
defineProps({
|
||||
clinicName: { type: String, required: true },
|
||||
currentPatient: { type: Object, default: null },
|
||||
queuePatients: { type: Array, default: () => [] },
|
||||
roomOptions: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
defineEmits(['pemeriksaan-awal', 'panggil-pemeriksaan', 'view-detail', 'action-pending', 'action-selesai', 'process-patient']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-room-column {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
background: var(--gp-bg, #F5F7FA);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-right: 16px;
|
||||
}
|
||||
.column-header {
|
||||
background: var(--gp-primary, #3F51B5);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.column-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.column-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: white; /* Make the whole column body white */
|
||||
}
|
||||
.empty-column-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
background: white;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-secondary, #6b7280);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="gp-room-monitor-card" :class="{ 'is-busy': isBusy }">
|
||||
<div class="card-header">
|
||||
<span class="room-name">{{ roomName }}</span>
|
||||
<span class="room-status" :class="isBusy ? 'text-danger' : 'text-success'">
|
||||
{{ isBusy ? 'SIBUK' : 'TERSEDIA' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<span v-if="isBusy" class="active-patient">{{ activePatient }} - {{ clinicName }}</span>
|
||||
<span v-else class="waiting-text">Menunggu pasien...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
roomName: { type: String, required: true },
|
||||
activePatient: { type: String, default: null }, // e.g. "AI002"
|
||||
clinicName: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const isBusy = computed(() => !!props.activePatient);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-room-monitor-card {
|
||||
background: var(--gp-surface, #fff);
|
||||
border: 1px solid var(--gp-border, #e5e7eb);
|
||||
border-left: 4px solid var(--gp-success, #10b981);
|
||||
border-radius: 2px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
|
||||
}
|
||||
.gp-room-monitor-card.is-busy {
|
||||
border-left-color: var(--gp-room-number, #F97316); /* Orange */
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.room-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--gp-text-primary, #1f2937);
|
||||
}
|
||||
.room-status {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.text-success { color: var(--gp-success, #10b981); }
|
||||
.text-danger { color: var(--gp-room-number, #F97316); } /* Matching image orange */
|
||||
|
||||
.card-body {
|
||||
font-size: 13px;
|
||||
}
|
||||
.active-patient {
|
||||
color: var(--gp-primary, #3F51B5);
|
||||
font-weight: 700;
|
||||
}
|
||||
.waiting-text {
|
||||
color: var(--gp-text-secondary, #6b7280);
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<span :class="['gp-badge', badgeClass]">
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
status: { type: String, required: true },
|
||||
text: { type: String, default: '' }
|
||||
});
|
||||
|
||||
const label = computed(() => props.text || props.status.toUpperCase());
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
const s = props.status.toLowerCase();
|
||||
if (s === 'pending' || s === 'sibuk') return 'gp-badge-danger';
|
||||
if (s === 'selesai' || s === 'tersedia' || s === 'proses') return 'gp-badge-success';
|
||||
return 'gp-badge-default';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gp-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
}
|
||||
.gp-badge-danger { background-color: var(--gp-danger, #ef4444); }
|
||||
.gp-badge-success { background-color: var(--gp-success, #10b981); }
|
||||
.gp-badge-default { background-color: var(--gp-text-secondary, #6b7280); }
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { useQueueStore } from '@/stores/queueStore';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
import { useClinicStore } from '@/stores/clinicStore';
|
||||
|
||||
export const useGrandPaviliun = () => {
|
||||
const queueStore = useQueueStore();
|
||||
const masterStore = useMasterStore();
|
||||
const clinicStore = useClinicStore();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
// Expose methods for UI
|
||||
const fetchEksekutifData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Pastikan data master ruangan sudah dimuat
|
||||
if (!masterStore.ruangData || masterStore.ruangData.length === 0) {
|
||||
// Assume it's loaded in index or master layouts, but fallback if not
|
||||
}
|
||||
|
||||
const eksekutifClinics = masterStore.ruangData.filter(r => r.jenisLayanan === 'Eksekutif');
|
||||
|
||||
// Ambil data antrean untuk semua klinik eksekutif
|
||||
const promises = eksekutifClinics.map(clinic => {
|
||||
queueStore.registerClinicInterest(clinic.kodeKlinik);
|
||||
return queueStore.fetchPatientsForClinic(clinic.kodeKlinik);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Connect WebSocket if not connected
|
||||
if (!queueStore.isWsConnected) {
|
||||
queueStore.initWebSocket('admin-klinik-ruang-eksekutif');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching eksekutif data:', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Computed data for UI
|
||||
const clinics = computed(() => {
|
||||
const eksekutifClinics = (masterStore.ruangData || []).filter(r => r.jenisLayanan === 'Eksekutif');
|
||||
|
||||
return eksekutifClinics.map(clinic => {
|
||||
// Find all patients for this clinic
|
||||
const clinicPatients = queueStore.allPatients.filter(p => p.kodeKlinik === clinic.kodeKlinik && p.processStage === 'klinik-ruang' && p.status !== 'processed');
|
||||
|
||||
// Find current processing patient (we take the first room's current patient if multiple, or global)
|
||||
// Since design shows 1 column per clinic, we find the first patient marked as processing.
|
||||
// Usually, queueStore.currentProcessingPatient is keyed by `klinik-ruang-${kodeKlinik}-${nomorRuang}`
|
||||
let currentPatientObj = null;
|
||||
|
||||
// We look through clinic rooms to find a processing patient
|
||||
for (const ruang of clinic.ruangList) {
|
||||
const key = `klinik-ruang-${clinic.kodeKlinik}-${ruang.nomorRuang}`;
|
||||
const processing = queueStore.currentProcessingPatient[key];
|
||||
if (processing) {
|
||||
currentPatientObj = {
|
||||
...processing,
|
||||
roomIndicator: `RUANG ${ruang.nomorRuang}`,
|
||||
nomorRuang: ruang.nomorRuang
|
||||
};
|
||||
break; // Stop at first one for the column view
|
||||
}
|
||||
}
|
||||
|
||||
// Get queue patients (those not currently processing)
|
||||
const currentPatientNo = currentPatientObj ? currentPatientObj.no : null;
|
||||
const queuePatients = clinicPatients
|
||||
.filter(p => p.no !== currentPatientNo)
|
||||
.map(p => ({
|
||||
...p,
|
||||
isActive: false // Could be based on selection later
|
||||
}));
|
||||
|
||||
return {
|
||||
kodeKlinik: clinic.kodeKlinik,
|
||||
namaKlinik: clinic.namaKlinik,
|
||||
currentPatient: currentPatientObj,
|
||||
queuePatients: queuePatients,
|
||||
roomOptions: clinic.ruangList
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const monitorRooms = computed(() => {
|
||||
const rooms = [];
|
||||
let idCounter = 1;
|
||||
|
||||
const eksekutifClinics = (masterStore.ruangData || []).filter(r => r.jenisLayanan === 'Eksekutif');
|
||||
|
||||
eksekutifClinics.forEach(clinic => {
|
||||
clinic.ruangList.forEach(ruang => {
|
||||
const key = `klinik-ruang-${clinic.kodeKlinik}-${ruang.nomorRuang}`;
|
||||
const processing = queueStore.currentProcessingPatient[key];
|
||||
|
||||
// As user specified: Monitoring only shows data when 'Panggil Pemeriksaan' (Tindakan) is true
|
||||
// Assuming calledTindakan flag exists on processing patient
|
||||
const isActiveInMonitoring = processing && processing.calledTindakan;
|
||||
|
||||
rooms.push({
|
||||
id: idCounter++,
|
||||
name: `RUANG ${ruang.nomorRuang}`,
|
||||
activePatient: isActiveInMonitoring ? (processing.noAntrian?.split(" |")[0] || processing.barcode) : null,
|
||||
clinicName: clinic.namaKlinik
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return rooms;
|
||||
});
|
||||
|
||||
// Actions
|
||||
const handlePemeriksaanAwal = (patient, kodeKlinik, nomorRuang) => {
|
||||
if (!patient || !kodeKlinik || !nomorRuang) return;
|
||||
|
||||
const roomInfo = masterStore.ruangData
|
||||
.find(c => c.kodeKlinik === kodeKlinik)
|
||||
?.ruangList.find(r => String(r.nomorRuang) === String(nomorRuang));
|
||||
|
||||
if (!roomInfo) return;
|
||||
|
||||
const wsData = {
|
||||
action: 'callPemeriksaanAwal',
|
||||
patientId: patient.barcode || patient.visitId,
|
||||
patientCode: patient.noAntrian?.split(" |")[0] || patient.barcode,
|
||||
patientName: patient.name || '',
|
||||
roomName: roomInfo.namaRuang,
|
||||
clinicCode: kodeKlinik,
|
||||
tipePanggilan: 'Pemeriksaan Awal'
|
||||
};
|
||||
|
||||
queueStore.sendViaPost(wsData);
|
||||
|
||||
// Update local state to mark as called
|
||||
if (patient.no) {
|
||||
const idx = queueStore.allPatients.findIndex(p => p.no === patient.no);
|
||||
if (idx !== -1) {
|
||||
queueStore.allPatients[idx].calledPemeriksaanAwal = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePanggilPemeriksaan = (patient, kodeKlinik, nomorRuang) => {
|
||||
if (!patient || !kodeKlinik || !nomorRuang) return;
|
||||
|
||||
const roomInfo = masterStore.ruangData
|
||||
.find(c => c.kodeKlinik === kodeKlinik)
|
||||
?.ruangList.find(r => String(r.nomorRuang) === String(nomorRuang));
|
||||
|
||||
if (!roomInfo) return;
|
||||
|
||||
const wsData = {
|
||||
action: 'callTindakan',
|
||||
patientId: patient.barcode || patient.visitId,
|
||||
patientCode: patient.noAntrian?.split(" |")[0] || patient.barcode,
|
||||
patientName: patient.name || '',
|
||||
roomName: roomInfo.namaRuang,
|
||||
clinicCode: kodeKlinik,
|
||||
tipePanggilan: 'Tindakan'
|
||||
};
|
||||
|
||||
queueStore.sendViaPost(wsData);
|
||||
|
||||
// Update local state to mark as called
|
||||
if (patient.no) {
|
||||
const idx = queueStore.allPatients.findIndex(p => p.no === patient.no);
|
||||
if (idx !== -1) {
|
||||
queueStore.allPatients[idx].calledTindakan = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processPatientAction = async (patient, action, kodeKlinik, nomorRuang) => {
|
||||
if (!patient || !kodeKlinik || !nomorRuang) return { success: false, message: 'Data tidak lengkap' };
|
||||
|
||||
const result = await queueStore.processPatientKlinikRuang(
|
||||
patient,
|
||||
action,
|
||||
kodeKlinik,
|
||||
nomorRuang
|
||||
);
|
||||
|
||||
if (action === 'pending' && result.success) {
|
||||
const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
|
||||
if (patientIndex !== -1) {
|
||||
queueStore.allPatients[patientIndex] = {
|
||||
...queueStore.allPatients[patientIndex],
|
||||
status: 'pending'
|
||||
};
|
||||
const key = `klinik-ruang-${kodeKlinik}-${nomorRuang}`;
|
||||
queueStore.currentProcessingPatient[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
clinics,
|
||||
monitorRooms,
|
||||
loading,
|
||||
fetchEksekutifData,
|
||||
handlePemeriksaanAwal,
|
||||
handlePanggilPemeriksaan,
|
||||
processPatientAction
|
||||
};
|
||||
};
|
||||
@@ -94,6 +94,14 @@ export const useQueue = (adminType = "loket", specificId = null) => {
|
||||
const isAssignedToThis = p.loketId && String(p.loketId) === String(targetId);
|
||||
if (isAssignedToThis) return true;
|
||||
|
||||
// Add strict isolation between Eksekutif and Reguler for unassigned tickets
|
||||
const isPatientEksekutif = p.noAntrian && (String(p.noAntrian).startsWith('E') || String(p.noAntrian).startsWith('F-E'));
|
||||
const isLoketEksekutif = thisLoket.tipeLoket === 'EKSEKUTIF' || thisLoket.id >= 1000;
|
||||
|
||||
if (isPatientEksekutif !== isLoketEksekutif) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isServedByThis = !p.loketId && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan) && thisLoket.pelayanan.includes(p.kodeKlinik);
|
||||
|
||||
return isServedByThis;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<AdminKlinikEksekutif v-if="jenisLayanan === 'Eksekutif'" />
|
||||
<div v-else>
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
icon="mdi-door-open"
|
||||
:title="`Admin Klinik Ruang - ${klinikData?.namaKlinik || ''}`"
|
||||
@@ -785,6 +787,7 @@
|
||||
:color="snackbarColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -798,6 +801,7 @@ 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 AdminKlinikEksekutif from '@/components/AdminKlinik/AdminKlinikEksekutif.vue';
|
||||
import { useThermalPrint } from '@/composables/useThermalPrint';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
+20
-26
@@ -709,28 +709,16 @@ const allPatientsForStage = computed(() => {
|
||||
|
||||
// Helper to check if patient belongs to this loket (for seed data only)
|
||||
const isPatientForThisLoket = (p) => {
|
||||
// Only check for EKSEKUTIF patients (seed data)
|
||||
const isPatientEksekutif =
|
||||
(p.pembayaran || "").toUpperCase().includes("EKSEKUTIF") ||
|
||||
(p.pembayaran || "").toUpperCase().includes("VIP");
|
||||
// 1. Strict isolation using ticket prefix
|
||||
const isPatientEksekutif = p.noAntrian && (String(p.noAntrian).startsWith('E') || String(p.noAntrian).startsWith('F-E'));
|
||||
|
||||
if (isPatientEksekutif !== isLoketEksekutif) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLoketEksekutif) {
|
||||
// Loket Eksekutif HANYA melayani pasien Eksekutif
|
||||
if (!isPatientEksekutif) return false;
|
||||
|
||||
// For EKSEKUTIF loket: accept all EKSEKUTIF patients
|
||||
// EKSEKUTIF lokets typically serve ALL clinics for executive patients
|
||||
// So we don't need to check kodeKlinik matching
|
||||
// Only check explicit loketId assignment if present
|
||||
if (p.loketId) {
|
||||
return String(p.loketId) === String(targetLoketId);
|
||||
}
|
||||
|
||||
// If no loketId assigned, accept all EKSEKUTIF patients
|
||||
return true;
|
||||
} else {
|
||||
// Loket Reguler TIDAK melayani pasien Eksekutif (this shouldn't happen with API data)
|
||||
if (isPatientEksekutif) return false;
|
||||
// 2. Explicit Loket assignment takes precedence
|
||||
if (p.loketId) {
|
||||
return String(p.loketId) === String(targetLoketId);
|
||||
}
|
||||
|
||||
// For REGULER seed data only: check loket assignment
|
||||
@@ -854,16 +842,22 @@ const filteredMenungguCount = computed(() => {
|
||||
if (isLoketEksekutif) {
|
||||
// For EKSEKUTIF, use seed data menunggu count (filtered by loket)
|
||||
return menungguPatients.value.filter((p) => {
|
||||
const isPatientEksekutif =
|
||||
(p.pembayaran || "").toUpperCase().includes("EKSEKUTIF") ||
|
||||
(p.pembayaran || "").toUpperCase().includes("VIP");
|
||||
// 1. Strict isolation using ticket prefix
|
||||
const isPatientEksekutif = p.noAntrian && (String(p.noAntrian).startsWith('E') || String(p.noAntrian).startsWith('F-E'));
|
||||
if (!isPatientEksekutif) return false;
|
||||
|
||||
// Accept all EKSEKUTIF patients if no explicit loketId
|
||||
// 2. Explicit Loket assignment takes precedence
|
||||
if (p.loketId) {
|
||||
return String(p.loketId) === String(targetLoketId);
|
||||
}
|
||||
return true;
|
||||
|
||||
// 3. Fallback to clinic mapping if unassigned
|
||||
const allowedServices = currentLoket?.pelayanan || [];
|
||||
if (p.kodeKlinik) {
|
||||
return allowedServices.includes(p.kodeKlinik);
|
||||
}
|
||||
|
||||
return false;
|
||||
}).length;
|
||||
} else {
|
||||
// For REGULER, count from allPatients (reactive)
|
||||
|
||||
+57
-17
@@ -875,11 +875,10 @@ const fetchPatientsForLoket = async (loketId: string | number, force: boolean =
|
||||
(loket?.namaLoket || '').toUpperCase().includes('EKSEKUTIF');
|
||||
|
||||
if (isEksekutif) {
|
||||
// Return EKSEKUTIF patients from seed data
|
||||
// Return EKSEKUTIF patients assigned to this loket
|
||||
return allPatients.value.filter(p => {
|
||||
const isPembayaranEksekutif = (p.pembayaran || '').toUpperCase().includes('EKSEKUTIF') ||
|
||||
(p.pembayaran || '').toUpperCase().includes('VIP');
|
||||
return isPembayaranEksekutif && p.processStage === 'loket';
|
||||
const isMatchingLoket = String(p.loketId) === String(loketId);
|
||||
return p.processStage === 'loket' && isMatchingLoket;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1485,6 +1484,20 @@ const fetchPatientsForLoket = async (loketId: string | number, force: boolean =
|
||||
// Enforce clinic mapping (pelayanan)
|
||||
if (thisLoket && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan)) {
|
||||
if (thisLoket.pelayanan.includes(p.kodeKlinik)) {
|
||||
// Bypass payment check for Eksekutif patients
|
||||
const isPatientEksekutif = p.noAntrian && (p.noAntrian.startsWith('E') || p.noAntrian.startsWith('F-E'));
|
||||
const isLoketEksekutif = thisLoket.tipeLoket === 'EKSEKUTIF' || thisLoket.id >= 1000;
|
||||
|
||||
if (isPatientEksekutif && isLoketEksekutif) {
|
||||
return true;
|
||||
}
|
||||
if (!isPatientEksekutif && isLoketEksekutif) {
|
||||
return false;
|
||||
}
|
||||
if (isPatientEksekutif && !isLoketEksekutif) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NEW: Check payment compatibility
|
||||
if (!isPaymentCompatible(p.pembayaran, thisLoket.pembayaran)) {
|
||||
return false;
|
||||
@@ -2609,11 +2622,22 @@ const fetchPatientsForLoket = async (loketId: string | number, force: boolean =
|
||||
let targetLoketName = oldPatient.loket;
|
||||
|
||||
if (newKlinik.kode) {
|
||||
const isEksekutif = (oldPatient.pembayaran || '').toUpperCase().includes('EKSEKUTIF') ||
|
||||
(oldPatient.pembayaran || '').toUpperCase().includes('VIP');
|
||||
|
||||
const allLokets = loketStore.lokets || [];
|
||||
const foundLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newKlinik.kode) || l.pelayanan.includes(newKlinik.kode?.split('-')[0]))
|
||||
);
|
||||
const foundLoket = allLokets.find(l => {
|
||||
const handlesClinic = l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newKlinik.kode) || l.pelayanan.includes(newKlinik.kode?.split('-')[0]));
|
||||
|
||||
if (!handlesClinic) return false;
|
||||
|
||||
if (isEksekutif) {
|
||||
return l.id >= 1000 || l.tipeLoket === 'EKSEKUTIF';
|
||||
} else {
|
||||
return l.id < 1000 && l.tipeLoket !== 'EKSEKUTIF';
|
||||
}
|
||||
});
|
||||
|
||||
if (foundLoket) {
|
||||
targetLoketId = foundLoket.id;
|
||||
@@ -2782,15 +2806,31 @@ const fetchPatientsForLoket = async (loketId: string | number, force: boolean =
|
||||
// fulfill requirement: "adjust based on loket id depending on creation"
|
||||
if (!newPatient.loketId && newPatient.kodeKlinik) {
|
||||
const allLokets = loketStore.lokets || [];
|
||||
const targetLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newPatient.kodeKlinik) || l.pelayanan.includes(newPatient.kodeKlinik?.split('-')[0]))
|
||||
);
|
||||
|
||||
if (targetLoket) {
|
||||
newPatient.loketId = targetLoket.id;
|
||||
if (!newPatient.loket) newPatient.loket = targetLoket.namaLoket;
|
||||
console.log(`✅ Auto-assigned Onsite Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
|
||||
if (isEksekutif) {
|
||||
const targetLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newPatient.kodeKlinik) || l.pelayanan.includes(newPatient.kodeKlinik?.split('-')[0])) &&
|
||||
(l.id >= 1000 || l.tipeLoket === 'EKSEKUTIF')
|
||||
);
|
||||
|
||||
if (targetLoket) {
|
||||
newPatient.loketId = targetLoket.id;
|
||||
if (!newPatient.loket) newPatient.loket = targetLoket.namaLoket;
|
||||
console.log(`✅ Auto-assigned Eksekutif Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
|
||||
} else {
|
||||
console.warn(`⚠️ Warning: No Eksekutif Loket found for clinic ${newPatient.kodeKlinik}. Ticket will not be routed to any loket.`);
|
||||
}
|
||||
} else {
|
||||
const targetLoket = allLokets.find(l =>
|
||||
l.pelayanan && Array.isArray(l.pelayanan) &&
|
||||
(l.pelayanan.includes(newPatient.kodeKlinik) || l.pelayanan.includes(newPatient.kodeKlinik?.split('-')[0]))
|
||||
);
|
||||
|
||||
if (targetLoket) {
|
||||
newPatient.loketId = targetLoket.id;
|
||||
if (!newPatient.loket) newPatient.loket = targetLoket.namaLoket;
|
||||
console.log(`✅ Auto-assigned Onsite Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user