update flow pasien, store & preview screen, source klinik, api dokter
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user