update flow pasien, store & preview screen, source klinik, api dokter

This commit is contained in:
bagus-arie05
2025-12-18 14:22:15 +07:00
parent d2a51f3aee
commit dfcd59481c
18 changed files with 984 additions and 183 deletions
@@ -32,7 +32,24 @@
<div v-else class="empty-state">
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
<div class="empty-text">Tidak ada pasien yang diproses</div>
<div class="empty-text mb-4">Tidak ada pasien yang diproses</div>
<v-btn
block
class="py-6 text-white"
color="primary-600"
size="large"
:disabled="!hasNextQueue"
@click="$emit('process-next')"
>
<v-icon start>mdi-play-circle</v-icon>
Proses Antrian Berikutnya
</v-btn>
<div v-if="nextQueueInfo" class="next-queue-info mt-3">
<div class="info-text">{{ nextQueueInfo }}</div>
</div>
<div v-else-if="!hasNextQueue" class="no-next-queue mt-3">
<div class="info-text">Tidak ada antrian di loket</div>
</div>
</div>
</v-card-text>
</v-card>
@@ -52,10 +69,18 @@ defineProps({
changeButtonText: {
type: String,
default: 'Ubah Klinik'
},
hasNextQueue: {
type: Boolean,
default: false
},
nextQueueInfo: {
type: String,
default: null
}
});
defineEmits(['action', 'change-klinik']);
defineEmits(['action', 'change-klinik', 'process-next']);
</script>
<style scoped lang="scss">
@@ -121,6 +146,20 @@ defineEmits(['action', 'change-klinik']);
margin-top: 8px;
}
.next-queue-info,
.no-next-queue {
text-align: center;
padding: 8px;
background: var(--color-neutral-200);
border-radius: 8px;
}
.info-text {
font-size: 12px;
color: var(--color-neutral-700);
font-weight: 600;
}
@media (max-width: 960px) {
.action-grid {
grid-template-columns: 1fr;
@@ -100,6 +100,7 @@ const handleCardClick = () => {
const getStatusColor = (status) => {
const colors = {
diloket: "var(--color-secondary-600)",
diproses: "var(--color-primary-600)",
terlambat: "var(--color-primary-600)",
pending: "var(--color-danger-600)"
};
@@ -109,6 +110,7 @@ const getStatusColor = (status) => {
const getStatusLabel = (status) => {
const labels = {
diloket: "Di Loket",
diproses: "Diproses",
terlambat: "Terlambat",
pending: "Pending"
};
@@ -29,7 +29,7 @@
color="success-600"
variant="outlined"
@click="$emit('call', 1)"
:disabled="!hasNext"
>
1
</v-btn>
+33 -6
View File
@@ -202,6 +202,10 @@ const props = defineProps({
type: Number,
default: 0
},
diprosesCount: {
type: Number,
default: 0
},
terlambatCount: {
type: Number,
default: 0
@@ -210,6 +214,10 @@ const props = defineProps({
type: Number,
default: 0
},
showDiproses: {
type: Boolean,
default: true
},
itemsPerPage: {
type: Number,
default: 9
@@ -219,6 +227,7 @@ const props = defineProps({
default: () => ({
all: 'Semua',
diloket: 'Di Loket',
diproses: 'Diproses',
terlambat: 'Terlambat',
pending: 'Pending'
})
@@ -249,12 +258,30 @@ const searchModel = computed({
}
});
const statusOptions = computed(() => [
{ value: 'all', label: props.statusLabels.all, count: props.items.length },
{ value: 'diloket', label: props.statusLabels.diloket, count: props.diLoketCount },
{ value: 'terlambat', label: props.statusLabels.terlambat, count: props.terlambatCount },
{ value: 'pending', label: props.statusLabels.pending, count: props.pendingCount }
]);
const statusOptions = computed(() => {
const baseOptions = [
{ value: 'all', label: props.statusLabels.all, count: props.items.length },
{ value: 'diloket', label: props.statusLabels.diloket, count: props.diLoketCount }
];
// Tampilkan "Diproses" hanya jika:
// - label-nya didefinisikan, DAN
// - komponen mengizinkan (showDiproses = true)
if (props.showDiproses && props.statusLabels.diproses) {
baseOptions.push({
value: 'diproses',
label: props.statusLabels.diproses,
count: props.diprosesCount
});
}
baseOptions.push(
{ value: 'terlambat', label: props.statusLabels.terlambat, count: props.terlambatCount },
{ value: 'pending', label: props.statusLabels.pending, count: props.pendingCount }
);
return baseOptions;
});
// Generate filter options from items
const klinikOptions = computed(() => {
+8 -2
View File
@@ -121,7 +121,7 @@ export const useQueue = (adminType = "loket") => {
};
const selectKlinik = (klinik) => {
const result = queueStore.createAntreanKlinik(klinik, adminType);
const result = queueStore.createAntreanKlinik(klinik, currentProcessingPatient.value, adminType);
showSnackbar(result.message, "success");
showKlinikDialog.value = false;
};
@@ -129,7 +129,7 @@ export const useQueue = (adminType = "loket") => {
const selectPenunjang = (penunjang) => {
const result = queueStore.createAntreanPenunjang(
penunjang,
selectedPatientForPenunjang.value,
currentProcessingPatient.value,
adminType
);
showSnackbar(result.message, "success");
@@ -154,6 +154,11 @@ export const useQueue = (adminType = "loket") => {
}
};
const processNextQueue = () => {
const result = queueStore.processNextQueue(adminType);
showSnackbar(result.message, result.success ? "success" : "warning");
};
const getRowClass = (item) => {
if (item.status === "current") {
return "text-success font-weight-bold";
@@ -197,6 +202,7 @@ export const useQueue = (adminType = "loket") => {
selectPenunjang,
openPenunjangDialog,
changeKlinik,
processNextQueue,
getRowClass,
};
};
+2 -2
View File
@@ -49,7 +49,7 @@ export default defineNuxtConfig({
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
public: {
authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001",
authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.114:3001",
// authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001",
},
},
@@ -64,7 +64,7 @@ export default defineNuxtConfig({
"~/assets/scss/main.scss",
],
devServer: {
host: "http://10.10.150.175", // Changed from "10.10.123.139"
host: "http://10.10.150.114", // Changed from "10.10.123.139"
port: 3001
},
+50 -23
View File
@@ -17,8 +17,11 @@
<CurrentPatientCard
:patient="currentProcessingPatient"
theme="secondary"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<!-- Queue Actions Card -->
@@ -32,28 +35,33 @@
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-btn
color="primary-600"
variant="flat"
block
size="large"
class="mb-2 create-btn"
@click="showKlinikDialog = true"
>
<v-icon start size="20">mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
<v-btn
color="secondary-600"
variant="flat"
block
size="large"
class="create-btn text-white"
@click="openPenunjangDialog()"
>
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
</div>
</v-col>
@@ -67,6 +75,7 @@
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:status-labels="statusLabels"
:show-diproses="false"
@action="handleTableAction"
/>
</v-col>
@@ -143,6 +152,7 @@ const {
callNext,
callMultiplePatients,
processPatient,
processNextQueue,
selectKlinik,
selectPenunjang,
openPenunjangDialog,
@@ -169,7 +179,7 @@ const statusLabels = {
pending: 'Pending'
};
// Combine all patients with status
// Combine all patients dengan status
const allPatients = computed(() => {
try {
const diLoketList = diLoketPatients.value || [];
@@ -187,6 +197,19 @@ const allPatients = computed(() => {
}
});
// Next queue info untuk CurrentPatientCard (Admin Klinik)
const nextQueueInfo = computed(() => {
const currentPatientNo = currentProcessingPatient.value?.no;
const nextPatient =
(diLoketPatients.value || []).find((p) => p.no !== currentPatientNo) ||
(diLoketPatients.value || [])[0];
if (nextPatient) {
return `Antrian berikutnya: ${nextPatient.noAntrian.split(" |")[0]}`;
}
return null;
});
const handlePatientAction = (action) => {
if (currentProcessingPatient.value) {
processPatient(currentProcessingPatient.value, action);
@@ -204,6 +227,10 @@ const handleCall = (count) => {
const handleTableAction = (item, action) => {
processPatient(item, action);
};
const handleProcessNext = () => {
processNextQueue();
};
</script>
<style scoped lang="scss">
+81 -44
View File
@@ -16,8 +16,11 @@
<!-- Current Patient Card -->
<CurrentPatientCard
:patient="currentProcessingPatient"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<!-- Queue Actions Card -->
@@ -31,29 +34,35 @@
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-btn
color="primary-600"
variant="flat"
block
size="large"
class="mb-2"
@click="showKlinikDialog = true"
>
<v-icon start size="20">mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
<v-btn
color="secondary-600"
variant="flat"
block
size="large"
class="text-white"
@click="openPenunjangDialog()"
>
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
</div>
</v-col>
<!-- Right Column: Patient Table -->
@@ -62,9 +71,11 @@
:items="allPatientsForStage"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
:di-loket-count="(diLoketPatients || []).length"
:di-loket-count="diLoketCount"
:diproses-count="currentProcessingPatient ? 1 : 0"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:show-diproses="false"
@action="handleTableAction"
/>
</v-col>
@@ -133,7 +144,6 @@ const {
diLoketPatients,
terlambatPatients,
pendingPatients,
waitingPatients,
nextPatient,
quotaUsed,
filteredKliniks,
@@ -146,6 +156,7 @@ const {
selectPenunjang,
openPenunjangDialog,
changeKlinik,
processNextQueue,
} = useQueue("loket");
const currentDate = ref(
@@ -162,34 +173,56 @@ const searchQuery = ref("");
// Combine all patients with status - PRESERVE ALL PROPERTIES
const allPatientsForStage = computed(() => {
const diLoket = (diLoketPatients.value || []).map(p => ({
...p, // Spread all properties first
status: 'diloket' // Then override only status
}));
const terlambat = (terlambatPatients.value || []).map(p => ({
...p,
status: 'terlambat'
}));
const pending = (pendingPatients.value || []).map(p => ({
...p,
status: 'pending'
}));
const currentPatientNo = currentProcessingPatient.value?.no;
const diLoket = (diLoketPatients.value || []).map((p) => ({
...p, // Spread all properties first
// If this patient is currently being processed, mark as "diproses", otherwise "diloket"
status: p.no === currentPatientNo ? "diproses" : "diloket",
}));
const terlambat = (terlambatPatients.value || []).map((p) => ({
...p,
status: "terlambat",
}));
const pending = (pendingPatients.value || []).map((p) => ({
...p,
status: "pending",
}));
// Do not show waiting patients in the table until called
const combined = [...diLoket, ...terlambat, ...pending];
// Debug: check if fastTrack property exists
console.log('📊 AdminLoket - allPatientsForStage:', combined.length);
console.log("📊 AdminLoket - allPatientsForStage:", combined.length);
if (combined.length > 0) {
console.log('📊 First patient:', combined[0]);
console.log('📊 First patient has fastTrack?', 'fastTrack' in combined[0]);
console.log('📊 First patient fastTrack value:', combined[0].fastTrack);
console.log('📊 All fastTrack values:', combined.map(p => p.fastTrack));
console.log("📊 First patient:", combined[0]);
console.log("📊 First patient has fastTrack?", "fastTrack" in combined[0]);
console.log("📊 First patient fastTrack value:", combined[0].fastTrack);
console.log(
"📊 All fastTrack values:",
combined.map((p) => p.fastTrack)
);
}
return combined;
});
// Count diLoket patients (excluding currently processing)
const diLoketCount = computed(() => {
const currentPatientNo = currentProcessingPatient.value?.no;
return (diLoketPatients.value || []).filter(p => p.no !== currentPatientNo).length;
});
// Next queue info for CurrentPatientCard
const nextQueueInfo = computed(() => {
const currentPatientNo = currentProcessingPatient.value?.no;
const nextPatient = (diLoketPatients.value || []).find(p => p.no !== currentPatientNo) || (diLoketPatients.value || [])[0];
if (nextPatient) {
return `Antrian berikutnya: ${nextPatient.noAntrian.split(" |")[0]}`;
}
return null;
});
const handlePatientAction = (action) => {
if (currentProcessingPatient.value) {
processPatient(currentProcessingPatient.value, action);
@@ -207,6 +240,10 @@ const handleCall = (count) => {
const handleTableAction = (item, action) => {
processPatient(item, action);
};
const handleProcessNext = () => {
processNextQueue();
};
</script>
<style scoped lang="scss">
@@ -234,4 +271,4 @@ const handleTableAction = (item, action) => {
padding: 12px;
}
}
</style>
</style>
+256 -40
View File
@@ -53,12 +53,10 @@
elevation="0"
>
<v-card-text class="clinic-content">
<!-- Clinic Name - HIERARCHY: Most Prominent -->
<h3 class="clinic-name">
{{ clinic.name }}
</h3>
<!-- Doctor Info -->
<div class="doctor-info">
<v-icon size="16" :color="clinic.available ? 'var(--color-primary-600)' : 'var(--color-neutral-600)'">
mdi-doctor
@@ -66,7 +64,6 @@
<span>{{ getDisplayDoctorInfo(clinic) }}</span>
</div>
<!-- Schedule Info - Fixed Bottom Left -->
<div class="schedule-info">
<v-icon size="14" :color="clinic.available ? 'var(--color-success-600)' : 'var(--color-neutral-600)'">
mdi-clock-outline
@@ -74,7 +71,6 @@
<span>{{ clinic.schedule || 'Tidak tersedia' }}</span>
</div>
<!-- Clinic Icon - Secondary Element -->
<div class="clinic-icon-wrapper">
<v-icon
:icon="clinic.icon"
@@ -95,7 +91,7 @@
</v-card-text>
</v-card>
<v-dialog v-model="showVisitTypeDialog" max-width="400" persistent>
<v-dialog v-model="showVisitTypeDialog" max-width="400" @click:outside="showVisitTypeDialog = false">
<v-card class="dialog-card">
<v-card-title class="dialog-header">
<v-icon class="mr-2">mdi-check-circle</v-icon>
@@ -118,7 +114,7 @@
<div class="doctor-list-section">
<p><strong>Dokter yang Tersedia:</strong></p>
<ul class="doctor-list">
<li v-for="(doctor, index) in selectedClinic.doctors" :key="index">
<li v-for="(doctor, index) in dialogDoctors" :key="index">
{{ doctor }}
</li>
</ul>
@@ -177,7 +173,7 @@
</v-card>
</v-dialog>
<v-dialog v-model="showPaymentTypeDialog" max-width="400">
<v-dialog v-model="showPaymentTypeDialog" max-width="400" @click:outside="showPaymentTypeDialog = false">
<v-card class="dialog-card">
<v-card-title class="dialog-header-simple">Jenis Pembayaran</v-card-title>
<v-divider />
@@ -197,7 +193,7 @@
</v-card>
</v-dialog>
<v-dialog v-model="showBookingFormDialog" max-width="500">
<v-dialog v-model="showBookingFormDialog" max-width="500" @click:outside="showBookingFormDialog = false">
<v-card class="dialog-card">
<v-card-title class="dialog-header">
Pilih Jadwal Kunjungan
@@ -267,10 +263,9 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, nextTick } from 'vue';
import { useRoute } from '#app';
import { useAnjunganStore } from '@/stores/anjunganStore';
import { useMasterStore } from '@/stores/masterStore';
import { useClinicStore } from '@/stores/clinicStore';
definePageMeta({
@@ -279,7 +274,6 @@ definePageMeta({
const route = useRoute();
const anjunganStore = useAnjunganStore();
const masterStore = useMasterStore();
const clinicStore = useClinicStore();
const anjunganId = computed(() => {
@@ -290,28 +284,251 @@ const anjunganId = computed(() => {
});
const anjunganData = computed(() => {
if (!anjunganId.value) return null;
return anjunganStore.getAnjunganById(anjunganId.value)?.value || null;
const id = anjunganId.value;
if (!id) return null;
const fromGetter = typeof anjunganStore.getAnjunganById === 'function'
? anjunganStore.getAnjunganById(id)
: null;
if (fromGetter?.value) {
return fromGetter.value;
}
const list =
anjunganStore.anjunganItems?.value ||
anjunganStore.anjunganItems ||
[];
if (!Array.isArray(list)) return null;
return list.find((a) => Number(a.id) === Number(id)) || null;
});
// ============================================================
// IMPROVED: API Integration dengan Manual Mapping Table
// ============================================================
// 🗺️ KODE MAPPING TABLE
// Mapping dari kode 2 karakter (store) ke kode lengkap (API)
// PENTING: Sesuaikan dengan kode yang sebenarnya digunakan di API Anda
const KODE_MAPPING = {
'AN': 'ANA', // Anak
'AS': 'ANESTESI', // Anestesi
'BD': 'BEDAH', // Bedah
'GR': 'GERIATRI', // Geriatri
'GI': 'GIGI', // Gigi dan Mulut
'GZ': 'GIZI', // Gizi
'HO': 'HEMATO', // Hemato Onkologi
'IP': 'INTERNA', // IPD / Penyakit Dalam
'JT': 'JANTUNG', // Jantung / Cardiologi
'JW': 'JIWA', // Jiwa / Psikiatri
'OB': 'OBGYN', // Kandungan
'KH': 'KEMOTERAPI', // Kemoterapi
'KN': 'NYERI', // Komplementer Nyeri
'KK': 'KULIT', // Kulit Kelamin
'MT': 'MATA', // Mata
'MC': 'MCU', // MCU
'ON': 'ONKOLOGI', // Onkologi
'PR': 'PARU', // Paru
'TD': 'TINDAKAN', // R. Tindakan
'RT': 'RADIOTERAPI', // Radioterapi
'RM': 'REHAB', // Rehab Medik
'SR': 'SARAF', // Saraf / Neurologi
'TH': 'THT', // THT
};
const todayString = new Date().toISOString().substring(0, 10);
const spesialisList = ref([]);
const doctorsByKode = ref({});
const loadingDoctors = ref({});
// Load list spesialis dari API
const loadSpesialisForDate = async (tanggal) => {
try {
const data = await $fetch(
`http://10.10.150.131:8088/api/jadwaldokter/tanggal/${tanggal}`
);
spesialisList.value = Array.isArray(data) ? data : [];
console.log(`✅ Loaded ${spesialisList.value.length} spesialis for ${tanggal}`);
} catch (error) {
console.error('❌ Gagal mengambil data spesialis dari API', error);
spesialisList.value = [];
}
};
// IMPROVED: Get Spesialis ID dengan Manual Mapping + Fallback
const getSpesialisIdForKode = (kodeKlinik) => {
if (!kodeKlinik) return null;
// 1. Normalize kode menggunakan mapping table
const kodeUpper = kodeKlinik.toUpperCase();
const normalizedKode = KODE_MAPPING[kodeUpper] || kodeUpper;
console.log(`🔄 Mapping: "${kodeKlinik}" → "${normalizedKode}"`);
// 2. Cari exact match dulu
let entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || '').toUpperCase();
return apiKode === normalizedKode;
});
// 3. Fallback: startsWith untuk fleksibilitas
if (!entry) {
entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || '').toUpperCase();
return apiKode.startsWith(normalizedKode);
});
if (entry) {
console.log(`⚠️ Using prefix match: "${normalizedKode}" → "${entry.Kode || entry.kode}"`);
}
}
const spesialisId = entry ? (entry.id || entry.ID || entry.FK_daftar_spesialis_ID) : null;
if (!spesialisId) {
console.warn(`❌ No match for: "${kodeKlinik}" (normalized: "${normalizedKode}")`);
console.log('Available API kodes:', spesialisList.value.map(s => s.Kode || s.kode));
} else {
console.log(`✅ Found spesialisId: ${spesialisId} for "${kodeKlinik}"`);
}
return spesialisId;
};
// IMPROVED: Load doctors dengan better error handling & caching
const loadDoctorsForKode = async (kodeKlinik) => {
if (!kodeKlinik) return;
// Cek apakah sudah ada data atau sedang loading
if (doctorsByKode.value[kodeKlinik] || loadingDoctors.value[kodeKlinik]) {
return;
}
loadingDoctors.value[kodeKlinik] = true;
const spesialisId = getSpesialisIdForKode(kodeKlinik);
if (!spesialisId) {
console.warn(`⚠️ Skip loading doctors untuk kode: ${kodeKlinik} (spesialisId not found)`);
loadingDoctors.value[kodeKlinik] = false;
// Set empty array agar tidak retry terus-menerus
doctorsByKode.value[kodeKlinik] = [];
return;
}
const tanggal = todayString;
try {
const data = await $fetch(
`http://10.10.150.131:8088/api/jadwaldokter/tanggal/${tanggal}/spesialis/${spesialisId}`
);
const list = Array.isArray(data) ? data : [];
const doctorNames = list
.map((d) => d.nama_lengkap || d.Nama_dokter || d.name)
.filter(Boolean);
doctorsByKode.value[kodeKlinik] = doctorNames;
console.log(`✅ Loaded ${doctorNames.length} doctors untuk ${kodeKlinik}:`, doctorNames);
} catch (error) {
console.error(`❌ Gagal mengambil dokter untuk kode ${kodeKlinik}:`, error);
// Set empty array agar tidak retry
doctorsByKode.value[kodeKlinik] = [];
} finally {
loadingDoctors.value[kodeKlinik] = false;
}
};
// IMPROVED: Prefetch dengan comprehensive logging
const prefetchDoctorsForCurrentAnjungan = async () => {
if (!anjunganData.value || !anjunganData.value.klinik) {
console.warn('⚠️ No anjungan data or klinik data available');
return;
}
const uniqueKodes = Array.from(new Set(anjunganData.value.klinik));
console.log(`🔄 Prefetching doctors for ${uniqueKodes.length} klinik:`, uniqueKodes);
await Promise.all(uniqueKodes.map((kode) => loadDoctorsForKode(kode)));
console.log('✅ Prefetch complete. Doctors by kode:', doctorsByKode.value);
};
// IMPROVED: Dialog doctors dengan explicit dependency
const dialogDoctors = computed(() => {
const kode = selectedClinic.value?.kode;
if (!kode) return [];
const apiDoctors = doctorsByKode.value[kode];
// Prioritas: API > Static
if (apiDoctors && apiDoctors.length > 0) {
return apiDoctors;
}
// Fallback ke static data
const staticDoctors = selectedClinic.value?.doctors || [];
if (staticDoctors.length > 0) {
console.log(`️ Using static doctors for ${kode} (API data not available)`);
}
return staticDoctors;
});
// IMPROVED: onMounted dengan sequential loading + debug
onMounted(async () => {
console.log('🚀 Component mounted, starting data load...');
// Step 1: Load spesialis list
await loadSpesialisForDate(todayString);
// Step 2: Wait for computed values to be ready
await nextTick();
// 🔍 DEBUG: Tampilkan comparison table
if (anjunganData.value?.klinik) {
console.group('📊 Kode Mapping Comparison');
console.table(
anjunganData.value.klinik.map(storeKode => {
const clinic = clinicStore.getClinicByKode(storeKode);
const normalizedKode = KODE_MAPPING[storeKode.toUpperCase()] || storeKode;
const spesialisId = getSpesialisIdForKode(storeKode);
const apiMatch = spesialisList.value.find(s =>
(s.id || s.ID || s.FK_daftar_spesialis_ID) === spesialisId
);
return {
'Store Kode': storeKode,
'Clinic Name': clinic?.name || '❌ Not found',
'Normalized': normalizedKode,
'API Kode': apiMatch ? (apiMatch.Kode || apiMatch.kode) : '❌ Not matched',
'Spesialis ID': spesialisId || '❌',
'Status': spesialisId ? '✅' : '❌'
};
})
);
console.groupEnd();
}
// Step 3: Prefetch doctors
await prefetchDoctorsForCurrentAnjungan();
console.log('✅ Initial data load complete');
});
// Filter clinics berdasarkan kode klinik yang dipilih di anjungan
const filteredClinics = computed(() => {
if (!anjunganData.value || !anjunganData.value.klinik || anjunganData.value.klinik.length === 0) {
return [];
}
// Ambil semua clinics dari store
const allClinics = clinicStore.getAllClinics;
// Filter berdasarkan kode klinik yang ada di anjungan
return allClinics.filter(clinic => {
// Cari kode klinik dari masterStore berdasarkan nama clinic
const klinikData = masterStore.klinikList.find(k => k.nama === clinic.name);
if (!klinikData) return false;
// Cek apakah kode klinik ada di list anjungan
return anjunganData.value.klinik.includes(klinikData.kode);
});
return anjunganData.value.klinik
.map((kode) => allClinics.find((c) => c.kode === kode) || null)
.filter(Boolean);
});
const showVisitTypeDialog = ref(false);
@@ -328,15 +545,20 @@ const bookingForm = ref({
payment: null,
});
// Function to display doctor info on card (max 2 doctors shown)
// IMPROVED: Display doctor info dengan prioritas API
const getDisplayDoctorInfo = (clinic) => {
if (!clinic.doctors || clinic.doctors.length === 0) {
const kode = clinic.kode;
const apiDoctors = kode ? doctorsByKode.value[kode] : null;
const doctors = (apiDoctors && apiDoctors.length > 0)
? apiDoctors
: (clinic.doctors || []);
if (!doctors || doctors.length === 0) {
return "Tidak ada dokter";
}
const maxDisplay = 2;
const doctors = clinic.doctors;
if (doctors.length <= maxDisplay) {
return doctors.join(", ");
} else {
@@ -346,26 +568,22 @@ const getDisplayDoctorInfo = (clinic) => {
}
};
// Check if Shift 1 is full
const isShift1Full = (clinic) => {
if (!clinic || !clinic.shifts || clinic.shifts.length === 0) return true;
const shift1 = clinic.shifts.find(s => s.name === "Shift 1");
return shift1 ? shift1.quota === 0 : true;
};
// Get current shift number
const getCurrentShiftNumber = () => {
return 1;
};
// Get current shift quota
const getCurrentShiftQuota = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return 0;
const currentShift = selectedClinic.value.shifts.find(s => s.name === "Shift 1");
return currentShift ? currentShift.quota : 0;
};
// Get available shifts for booking form
const getAvailableShiftsForBooking = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return [];
@@ -387,12 +605,10 @@ const getAvailableShiftsForBooking = () => {
}));
};
// Get minimum date (today)
const getMinDate = () => {
return new Date().toISOString().substring(0, 10);
};
// Get maximum date (2 weeks from today)
const getMaxDate = () => {
const today = new Date();
const maxDate = new Date(today);
@@ -407,10 +623,10 @@ const showSnackbar = (text, color = "success") => {
};
const selectClinic = (clinic) => {
if (clinic.available) {
selectedClinic.value = clinic;
showVisitTypeDialog.value = true;
}
if (!clinic || !clinic.available) return;
selectedClinic.value = clinic;
showVisitTypeDialog.value = true;
};
const selectVisitType = (type) => {
+22 -3
View File
@@ -133,11 +133,12 @@ definePageMeta({
layout: false,
});
const route = useRoute();
const queueStore = useQueueStore();
const screenStore = useScreenStore();
const masterStore = useMasterStore();
const route = useRoute();
const screenId = computed(() => {
const id = route.params.id;
// Handle both string and array cases
@@ -147,8 +148,26 @@ const screenId = computed(() => {
});
const screenData = computed(() => {
if (!screenId.value) return null;
return screenStore.getScreenById(screenId.value)?.value || null;
const id = screenId.value;
if (!id) return null;
// Utamakan getter store (getScreenById)
const fromGetter = typeof screenStore.getScreenById === 'function'
? screenStore.getScreenById(id)
: null;
if (fromGetter?.value) {
return fromGetter.value;
}
// Fallback: baca langsung dari state mentah
const list =
screenStore.screenItems?.value ||
screenStore.screenItems ||
[];
if (!Array.isArray(list)) return null;
return list.find((s) => Number(s.id) === Number(id)) || null;
});
const currentTime = ref("");
+117
View File
@@ -61,6 +61,18 @@
</div>
</template>
<template v-slot:item.layarInformasi="{ item }">
<v-btn
size="small"
@click="openPreviewDialog(item)"
variant="flat"
class="btn-preview mr-2"
>
<v-icon size="16" left>mdi-eye</v-icon>
Preview
</v-btn>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
size="small"
@@ -182,6 +194,35 @@
</v-card>
</v-dialog>
<!-- Preview Dialog -->
<v-dialog v-model="previewDialog" max-width="95vw" max-height="95vh" persistent>
<v-card class="preview-dialog-card">
<v-card-title class="preview-dialog-header">
<div class="preview-header-content">
<v-icon size="24" class="mr-2">mdi-monitor-dashboard</v-icon>
<span class="headline-4">Preview Anjungan: {{ previewItem?.namaAnjungan || '' }}</span>
</div>
<v-btn icon variant="text" size="small" class="btn-close" @click="closePreviewDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="preview-content">
<iframe
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
frameborder="0"
allowfullscreen
></iframe>
<div v-else class="preview-loading">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<p class="mt-4">Memuat preview...</p>
</div>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
@@ -204,6 +245,9 @@ const clinicStore = useClinicStore();
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
const previewDialog = ref(false);
const previewItem = ref(null);
const previewUrl = ref('');
const snackbar = ref({
show: false,
@@ -216,6 +260,7 @@ const headers = ref([
{ title: 'Nama Anjungan', value: 'namaAnjungan', sortable: true },
{ title: 'Jenis Pasien', value: 'jenisPasien', sortable: false },
{ title: 'Klinik Ditampilkan', value: 'klinik', sortable: false },
{ title: 'Layar Informasi', value: 'layarInformasi', sortable: false, width: '150px' },
{ title: 'Actions', value: 'actions', sortable: false, width: 'auto' },
]);
@@ -296,6 +341,19 @@ const handleDelete = (item) => {
};
}
};
const openPreviewDialog = (item) => {
previewItem.value = item;
// Generate preview URL untuk anjungan dengan ID
previewUrl.value = `/anjungan/anjungan/${item.id}`;
previewDialog.value = true;
};
const closePreviewDialog = () => {
previewDialog.value = false;
previewItem.value = null;
previewUrl.value = '';
};
</script>
<style scoped lang="scss">
@@ -543,5 +601,64 @@ $font-weight-semibold: 600;
line-height: 24px;
min-width: 100px;
}
// ============================================
// PREVIEW DIALOG
// ============================================
.btn-preview {
background-color: $success-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
.preview-dialog-card {
font-family: $font-family-base;
height: 95vh;
display: flex;
flex-direction: column;
}
.preview-dialog-header {
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
color: $neutral-100;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header-content {
display: flex;
align-items: center;
}
.preview-content {
padding: 0 !important;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: $neutral-800;
overflow: hidden;
}
.preview-iframe {
width: 100%;
height: 100%;
min-height: 80vh;
border: none;
}
.preview-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $neutral-100;
padding: 40px;
}
</style>
+117
View File
@@ -50,6 +50,18 @@
<span class="ml-2 body-3 text-medium">{{ item.namaKlinik }}</span>
</template>
<template v-slot:item.layarInformasi="{ item }">
<v-btn
size="small"
@click="openPreviewDialog(item)"
variant="flat"
class="btn-preview mr-2"
>
<v-icon size="16" left>mdi-eye</v-icon>
Preview
</v-btn>
</template>
<template v-slot:item.aksi="{ item }">
<v-btn
size="small"
@@ -223,6 +235,35 @@
</v-card>
</v-dialog>
<!-- Preview Dialog -->
<v-dialog v-model="previewDialog" max-width="95vw" max-height="95vh" persistent>
<v-card class="preview-dialog-card">
<v-card-title class="preview-dialog-header">
<div class="preview-header-content">
<v-icon size="24" class="mr-2">mdi-door-open</v-icon>
<span class="headline-4">Preview Klinik Ruang: {{ previewItem?.namaKlinik || '' }}</span>
</div>
<v-btn icon variant="text" size="small" class="btn-close" @click="closePreviewDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="preview-content">
<iframe
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
frameborder="0"
allowfullscreen
></iframe>
<div v-else class="preview-loading">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<p class="mt-4">Memuat preview...</p>
</div>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
@@ -241,6 +282,9 @@ const masterStore = useMasterStore();
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
const previewDialog = ref(false);
const previewItem = ref(null);
const previewUrl = ref('');
const snackbar = ref({
show: false,
@@ -253,6 +297,7 @@ const headers = ref([
{ title: 'Nama Klinik', value: 'namaKlinik', sortable: true },
{ title: 'Kode', value: 'kodeKlinik', sortable: true },
{ title: 'Nama Ruang', value: 'namaRuang', sortable: true },
{ title: 'Layar Informasi', value: 'layarInformasi', sortable: false, width: '150px' },
{ title: 'Aksi', value: 'aksi', sortable: false },
]);
@@ -361,6 +406,19 @@ const handleDelete = (item) => {
};
}
};
const openPreviewDialog = (item) => {
previewItem.value = item;
// Generate preview URL untuk klinik ruang menggunakan kodeKlinik
previewUrl.value = `/anjungan/antrianklinikruang/${item.kodeKlinik}`;
previewDialog.value = true;
};
const closePreviewDialog = () => {
previewDialog.value = false;
previewItem.value = null;
previewUrl.value = '';
};
</script>
<style scoped lang="scss">
@@ -648,6 +706,65 @@ $font-weight-semibold: 600;
min-width: 100px;
}
// ============================================
// PREVIEW DIALOG
// ============================================
.btn-preview {
background-color: $secondary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
.preview-dialog-card {
font-family: $font-family-base;
height: 95vh;
display: flex;
flex-direction: column;
}
.preview-dialog-header {
background: linear-gradient(135deg, $success-600 0%, $success-700 100%);
color: $neutral-100;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header-content {
display: flex;
align-items: center;
}
.preview-content {
padding: 0 !important;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: $neutral-800;
overflow: hidden;
}
.preview-iframe {
width: 100%;
height: 100%;
min-height: 80vh;
border: none;
}
.preview-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $neutral-100;
padding: 40px;
}
// ============================================
// RESPONSIVE
// ============================================
+117
View File
@@ -57,6 +57,18 @@
</div>
</template>
<template v-slot:item.layarInformasi="{ item }">
<v-btn
size="small"
@click="openPreviewDialog(item)"
variant="flat"
class="btn-preview mr-2"
>
<v-icon size="16" left>mdi-eye</v-icon>
Preview
</v-btn>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
size="small"
@@ -214,6 +226,35 @@
</v-card>
</v-dialog>
<!-- Preview Dialog -->
<v-dialog v-model="previewDialog" max-width="95vw" max-height="95vh" persistent>
<v-card class="preview-dialog-card">
<v-card-title class="preview-dialog-header">
<div class="preview-header-content">
<v-icon size="24" class="mr-2">mdi-monitor</v-icon>
<span class="headline-4">Preview Screen: {{ previewItem?.namaScreen || '' }}</span>
</div>
<v-btn icon variant="text" size="small" class="btn-close" @click="closePreviewDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="preview-content">
<iframe
v-if="previewUrl"
:src="previewUrl"
class="preview-iframe"
frameborder="0"
allowfullscreen
></iframe>
<div v-else class="preview-loading">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<p class="mt-4">Memuat preview...</p>
</div>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
@@ -234,6 +275,9 @@ const screenStore = useScreenStore();
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
const previewDialog = ref(false);
const previewItem = ref(null);
const previewUrl = ref('');
const snackbar = ref({
show: false,
@@ -246,6 +290,7 @@ const headers = ref([
{ title: "Nama Screen", value: "namaScreen", sortable: true },
{ title: "Nomor Screen", value: "nomorScreen", sortable: true },
{ title: "Klinik Ditampilkan", value: "klinik", sortable: false },
{ title: "Layar Informasi", value: "layarInformasi", sortable: false, width: "150px" },
{ title: "Actions", value: "actions", sortable: false, width: "auto" },
]);
@@ -328,6 +373,19 @@ const handleDelete = (item) => {
};
}
};
const openPreviewDialog = (item) => {
previewItem.value = item;
// Generate preview URL untuk screen dengan ID
previewUrl.value = `/anjungan/antrianklinik/${item.id}`;
previewDialog.value = true;
};
const closePreviewDialog = () => {
previewDialog.value = false;
previewItem.value = null;
previewUrl.value = '';
};
</script>
<style scoped lang="scss">
@@ -634,4 +692,63 @@ $font-weight-semibold: 600;
line-height: 24px;
min-width: 100px;
}
// ============================================
// PREVIEW DIALOG
// ============================================
.btn-preview {
background-color: $success-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
.preview-dialog-card {
font-family: $font-family-base;
height: 95vh;
display: flex;
flex-direction: column;
}
.preview-dialog-header {
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
color: $neutral-100;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header-content {
display: flex;
align-items: center;
}
.preview-content {
padding: 0 !important;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: $neutral-800;
overflow: hidden;
}
.preview-iframe {
width: 100%;
height: 100%;
min-height: 80vh;
border: none;
}
.preview-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $neutral-100;
padding: 40px;
}
</style>
+38 -13
View File
@@ -7,6 +7,7 @@ export const useClinicStore = defineStore('clinic', () => {
const clinics = ref([
{
id: 1,
kode: "AN",
name: "ANAK",
subtitle: "",
icon: "mdi-baby-face",
@@ -21,6 +22,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 2,
kode: "AS",
name: "ANESTESI",
subtitle: "",
icon: "mdi-face-mask",
@@ -35,6 +37,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 3,
kode: "BD",
name: "BEDAH",
subtitle: "",
icon: "mdi-medical-bag",
@@ -49,6 +52,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 4,
kode: "GR",
name: "GERIATRI",
subtitle: "",
icon: "mdi-human-cane",
@@ -63,6 +67,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 5,
kode: "GI",
name: "GIGI DAN MULUT",
subtitle: "",
icon: "mdi-tooth",
@@ -77,6 +82,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 6,
kode: "GZ",
name: "GIZI",
subtitle: "",
icon: "mdi-food-apple",
@@ -91,6 +97,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 7,
kode: "HO",
name: "HEMATO ONKOLOGI MEDIS",
subtitle: "",
icon: "mdi-water",
@@ -102,6 +109,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 8,
kode: "IP",
name: "IPD (PENYAKIT DALAM)",
subtitle: "",
icon: "mdi-hospital",
@@ -116,6 +124,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 9,
kode: "JT",
name: "JANTUNG (CARDIOLOGI)",
subtitle: "",
icon: "mdi-heart-pulse",
@@ -130,6 +139,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 10,
kode: "JW",
name: "JIWA",
subtitle: "",
icon: "mdi-head-dots-horizontal",
@@ -144,6 +154,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 11,
kode: "OB",
name: "KANDUNGAN",
subtitle: "",
icon: "mdi-human-pregnant",
@@ -158,6 +169,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 12,
kode: "KH", // kode fiktif untuk KEMOTERAPI (belum ada di master klinik)
name: "KEMOTERAPI",
subtitle: "",
icon: "mdi-virus",
@@ -172,6 +184,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 13,
kode: "KN", // kode fiktif untuk KOMPLEMENTER (NYERI)
name: "KOMPLEMENTER (NYERI)",
subtitle: "",
icon: "mdi-medical-bag",
@@ -186,6 +199,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 14,
kode: "KK",
name: "KULIT KELAMIN",
subtitle: "",
icon: "mdi-human-male-female",
@@ -200,6 +214,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 15,
kode: "MT",
name: "MATA",
subtitle: "",
icon: "mdi-eye",
@@ -214,6 +229,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 16,
kode: "MC", // kode fiktif untuk MCU
name: "MCU",
subtitle: "",
icon: "mdi-clipboard-check",
@@ -228,6 +244,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 17,
kode: "ON", // kode fiktif untuk ONKOLOGI
name: "ONKOLOGI",
subtitle: "",
icon: "mdi-virus",
@@ -242,6 +259,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 18,
kode: "PR",
name: "PARU",
subtitle: "",
icon: "mdi-lungs",
@@ -256,6 +274,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 19,
kode: "TD", // mengikuti kode pelayanan TD di loket
name: "R. TINDAKAN (EMG, ECG, DLL)",
subtitle: "",
icon: "mdi-waveform",
@@ -270,6 +289,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 20,
kode: "RT",
name: "RADIOTERAPI",
subtitle: "",
icon: "mdi-radioactive",
@@ -284,6 +304,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 21,
kode: "RM",
name: "REHAB MEDIK",
subtitle: "",
icon: "mdi-human-cane",
@@ -298,6 +319,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 22,
kode: "SR",
name: "SARAF (NEUROLOGI)",
subtitle: "",
icon: "mdi-head-cog",
@@ -309,6 +331,7 @@ export const useClinicStore = defineStore('clinic', () => {
},
{
id: 23,
kode: "TH", // kode fiktif untuk THT
name: "THT",
subtitle: "",
icon: "mdi-ear-hearing",
@@ -331,26 +354,28 @@ export const useClinicStore = defineStore('clinic', () => {
return clinics.value.find(c => c.name === name);
};
// Get clinic by kode (lebih stabil untuk integrasi dengan master klinik & anjungan)
const getClinicByKode = (kode) => {
return clinics.value.find(c => c.kode === kode);
};
// Get clinics list untuk dropdown (format: { name, kode })
// Mapping name ke kode klinik dari masterStore
const getClinicsForDropdown = (masterStore) => {
return clinics.value.map(clinic => {
// Cari kode dari masterStore berdasarkan nama
const klinikData = masterStore.klinikList.find(k => k.nama === clinic.name);
return {
name: clinic.name,
kode: klinikData?.kode || clinic.name.substring(0, 2).toUpperCase(),
icon: clinic.icon,
available: clinic.available,
id: clinic.id
};
});
// Sekarang langsung pakai kode dari clinic, tidak bergantung ke nama di masterStore
const getClinicsForDropdown = () => {
return clinics.value.map(clinic => ({
name: clinic.name,
kode: clinic.kode,
icon: clinic.icon,
available: clinic.available,
id: clinic.id
}));
};
return {
clinics,
getAllClinics,
getClinicByName,
getClinicByKode,
getClinicsForDropdown,
};
});
+14 -18
View File
@@ -1,26 +1,22 @@
// stores/klinikRuangStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useClinicStore } from './clinicStore';
export const useKlinikRuangStore = defineStore('klinikRuang', () => {
// State - List Master Klinik
const masterKlinikList = ref([
{ kode: 'AN', nama: 'ANAK' },
{ kode: 'AS', nama: 'ANESTESI' },
{ kode: 'BD', nama: 'BEDAH' },
{ kode: 'GR', nama: 'GERIATRI' },
{ kode: 'GI', nama: 'GIGI DAN MULUT' },
{ kode: 'GZ', nama: 'GIZI' },
{ kode: 'HO', nama: 'HOM' },
{ kode: 'IP', nama: 'IPD' },
{ kode: 'JT', nama: 'JANTUNG' },
{ kode: 'JW', nama: 'JIWA' },
{ kode: 'KK', nama: 'KULIT KELAMIN' },
{ kode: 'MT', nama: 'MATA' },
{ kode: 'OB', nama: 'OBGYN' },
{ kode: 'PR', nama: 'PARU' },
{ kode: 'RT', nama: 'RADIOTERAPI' },
]);
const clinicStore = useClinicStore();
// State/Computed - List Master Klinik (disinkronkan dengan clinicStore)
const masterKlinikList = computed(() => {
const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
? clinicStore.getClinicsForDropdown()
: [];
return baseList.map((c) => ({
kode: c.kode,
nama: c.name,
}));
});
// State - Klinik Ruang Data
const klinikRuangList = ref([
+32 -8
View File
@@ -1,8 +1,12 @@
// stores/masterStore.js - Integrated dengan Penunjang
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useClinicStore } from './clinicStore';
export const useMasterStore = defineStore('master', () => {
// Store referensi utama daftar klinik (single source of truth)
const clinicStore = useClinicStore();
// ============================================
// MASTER KLINIK
// ============================================
@@ -74,13 +78,24 @@ export const useMasterStore = defineStore('master', () => {
]);
// Computed - Get klinik list for dropdowns
const klinikList = computed(() =>
klinikData.value.map(k => ({
id: k.id,
kode: k.kode,
nama: k.nama
}))
);
// Sumber data kode & nama klinik diambil dari clinicStore (1 pintu),
// lalu difilter hanya untuk kode yang terdaftar di master klinik ini.
const klinikList = computed(() => {
const availableCodes = new Set(klinikData.value.map((k) => k.kode));
// getClinicsForDropdown() sudah mengembalikan { id, name, kode, icon, available }
const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
? clinicStore.getClinicsForDropdown()
: [];
return baseList
.filter((c) => availableCodes.has(c.kode))
.map((c) => ({
id: c.id,
kode: c.kode,
nama: c.name,
}));
});
// Actions - Klinik
const addKlinik = (klinikPayload) => {
@@ -611,7 +626,16 @@ export const useMasterStore = defineStore('master', () => {
// UTILITY FUNCTIONS
// ============================================
const getKlinikNameByKode = (kode) => {
const klinik = klinikData.value.find(k => k.kode === kode);
// Utamakan nama dari clinicStore agar konsisten di seluruh aplikasi
if (typeof clinicStore.getClinicByKode === 'function') {
const clinic = clinicStore.getClinicByKode(kode);
if (clinic) {
return clinic.name;
}
}
// Fallback ke data lokal master jika belum terdaftar di clinicStore
const klinik = klinikData.value.find((k) => k.kode === kode);
return klinik ? klinik.nama : kode;
};
+6 -6
View File
@@ -49,12 +49,12 @@ const defaultNavItems: NavItem[] = [
children: [
{ id: 15, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-circle-small" },
{ id: 16, name: "User Login", path: "/setting/UserLogin", icon: "mdi-circle-small" },
{ id: 17, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
{ id: 18, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
{ id: 19, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
{ id: 20, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
{ id: 21, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
{ id: 22, name: "Master Anjungan", path: "/setting/masteranjungan", icon: "mdi-circle-small" },
{ id: 17, name: "Master Anjungan", path: "/setting/masteranjungan", icon: "mdi-circle-small" },
{ id: 18, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
{ id: 19, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
{ id: 20, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
{ id: 21, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
{ id: 22, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
],
},
];
+47 -15
View File
@@ -1,8 +1,10 @@
// stores/queueStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useClinicStore } from './clinicStore';
export const useQueueStore = defineStore('queue', () => {
const clinicStore = useClinicStore();
// Seed data for easy reset during dev
const seedPatients = [
{
@@ -175,17 +177,21 @@ export const useQueueStore = defineStore('queue', () => {
penunjang: null,
});
const kliniks = ref([
{ id: 1, name: "KANDUNGAN" },
{ id: 2, name: "IPD" },
{ id: 3, name: "THT" },
{ id: 4, name: "SARAF" },
{ id: 5, name: "JIWA" },
{ id: 6, name: "BEDAH" },
{ id: 7, name: "MATA" },
{ id: 8, name: "ANAK" },
{ id: 9, name: "KULIT" },
]);
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore
const kliniks = computed(() => {
const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
? clinicStore.getClinicsForDropdown()
: [];
// Bentuk objek disesuaikan dengan yang dipakai di useQueue (id, name, kode)
return baseList.map((c) => ({
id: c.id,
name: c.name,
kode: c.kode,
icon: c.icon,
available: c.available,
}));
});
const penunjangs = ref([
{ id: 1, name: "LABORATORIUM" },
@@ -261,7 +267,6 @@ export const useQueueStore = defineStore('queue', () => {
const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
if (index !== -1) {
allPatients.value[index] = { ...allPatients.value[index], status: "di-loket" };
currentProcessingPatient.value[adminType] = allPatients.value[index];
}
quotaUsed.value++;
@@ -401,10 +406,35 @@ export const useQueueStore = defineStore('queue', () => {
return { success: true, message };
};
const createAntreanKlinik = (klinik, adminType = 'loket') => {
const processNextQueue = (adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const nextPatient = allPatients.value.find(p =>
p.status === 'di-loket' && p.processStage === targetStage
);
if (!nextPatient) {
return { success: false, message: "Tidak ada pasien di loket yang dapat diproses" };
}
// Set sebagai current processing patient
currentProcessingPatient.value[adminType] = nextPatient;
return {
success: true,
message: `Memproses pasien ${nextPatient.noAntrian.split(" |")[0]}`,
};
};
const createAntreanKlinik = (klinik, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = `250811${String(timestamp.getTime()).slice(-6)}`;
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const newPatient = {
no: newNo,
@@ -416,10 +446,11 @@ export const useQueueStore = defineStore('queue', () => {
shift: "Shift 1",
klinik: klinik.name,
fastTrack: "TIDAK",
pembayaran: "UMUM",
pembayaran: patient ? patient.pembayaran : "UMUM",
status: "di-loket",
processStage: "klinik",
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
};
allPatients.value.push(newPatient);
@@ -515,6 +546,7 @@ export const useQueueStore = defineStore('queue', () => {
createAntreanKlinik,
createAntreanPenunjang,
changeKlinik,
processNextQueue,
getPatientsByStage,
getTotalPasienByStage,
getCurrentProcessing,