Update flow anjungan

This commit is contained in:
bagus-arie05
2025-10-21 08:31:00 +07:00
parent 71890ffb4c
commit d7f3240a82
11 changed files with 2665 additions and 2864 deletions

No files matched your search

-1
View File
@@ -5,7 +5,6 @@
location="top end"
origin="bottom right"
transition="slide-y-transition"
open-on-hover
>
<template v-slot:activator="{ props: menuProps }">
<v-list-item
+151 -213
View File
@@ -1,249 +1,187 @@
<!-- components/sideBar.vue -->
<template>
<v-navigation-drawer
v-model="drawer"
:model-value="drawer"
:rail="rail"
permanent
app
color="white"
width="280"
rail-width="72"
class="bg-white d-flex flex-column" @update:model-value="emit('update:drawer', $event)"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<div class="pa-4 d-flex align-center" style="height: 64px">
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
Antrean RSSA
</v-toolbar-title>
</div>
<v-list-item
class="py-4 px-2"
:class="{'text-center': rail}"
>
<div class="d-flex align-center" :class="{'justify-center': rail}">
<v-icon color="orange-darken-2" size="36">mdi-hospital-box-outline</v-icon>
<v-list-item-title v-if="!rail" class="ml-2 text-h6 font-weight-bold text-blue-grey-darken-4">
<span class="text-orange-darken-2">Antrian</span> RSSA
</v-list-item-title>
</div>
</v-list-item>
<v-divider></v-divider>
<v-list density="default" nav class="py-2">
<!-- search bar
<v-list-item
prepend-icon="mdi-magnify"
:title="!rail ? 'Search' : ''"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item> -->
<v-list
density="compact"
nav
class="mt-2 flex-grow-1" v-model:opened="openedGroups"
>
<template v-for="item in items" :key="item.name">
<v-list-group
v-if="item.children"
:value="item.name"
:disabled="rail"
>
<template v-slot:activator="{ props: groupProps }">
<v-list-item
v-bind="groupProps"
:prepend-icon="item.icon"
:title="item.name"
color="orange-darken-2"
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
></v-list-item>
</template>
<template v-for="item in navItemsStore.getNavItems" :key="item.id">
<v-menu
v-if="item.children && item.children.length > 0"
open-on-hover
location="end"
:disabled="!rail"
>
<template v-slot:activator="{ props }">
<v-list-group v-if="!rail" :value="item.name">
<template v-slot:activator="{ props: listGroupProps }">
<v-list-item
v-bind="listGroupProps"
:prepend-icon="item.icon"
:title="item.name"
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1 pl-12"
>
</v-list-item>
v-for="child in item.children"
:key="child.name"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="pl-8"
color="orange-darken-2"
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
></v-list-item>
</v-list-group>
<v-list-item
v-else
v-bind="props"
:prepend-icon="item.icon"
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
<v-tooltip
v-else
:disabled="!rail"
open-on-hover
location="end"
:text="item.name"
>
</v-list-item>
</template>
<v-list class="py-2" style="min-width: 200px">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ item.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2"
>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-else
:prepend-icon="item.icon"
:title="!rail ? item.name : ''"
:to="item.path"
:active="item.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1"
>
<template v-slot:append v-if="item.badge && !rail">
<v-chip size="x-small" color="error" variant="flat">{{
item.badge
}}</v-chip>
</template>
</v-list-item>
</template>
<v-divider class="my-4 mx-2"></v-divider>
<template #activator="{ props: tooltipProps }">
<v-list-item
v-bind="tooltipProps"
:prepend-icon="item.icon"
:title="item.name"
:to="item.path"
link
color="orange-darken-2"
active-class="bg-orange-lighten-5 text-orange-darken-2 font-weight-bold"
></v-list-item>
</template>
</v-tooltip>
</template>
</v-list>
<v-divider></v-divider>
<v-list v-if="user" nav density="compact" class="pa-2 flex-shrink-0">
<ProfilePopup
:user="user"
@logout="handleLogout"
:rail="rail"
/>
</v-list>
<v-list v-else nav density="compact" class="flex-shrink-0">
<v-list-item
@click="redirectToLogin"
prepend-icon="mdi-login"
title="Login"
link
color="blue-grey-darken-4"
></v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, onMounted, ref, watch } from 'vue';
import { navigateTo } from '#app';
import ProfilePopup from './ProfilePopup.vue';
import { useAuth } from '~/composables/useAuth';
<script setup>
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
import { useNavItemsStore } from "@/stores/navItems";
import ProfilePopup from "./ProfilePopup.vue";
import { useAuth } from "~/composables/useAuth";
const { user, logout, checkAuth } = useAuth();
// State
const drawer = ref(true);
const rail = ref(true);
const hoverTimeout = ref(null);
interface NavItem {
id: number;
name: string;
path: string;
icon: string;
children?: NavItem[];
}
// Akses Pinia Store
const user = ref({
name: 'Adam Sulfat',
email: '[email protected]',
picture: 'https://i.pravatar.cc/150?img=33',
id: 'a1b2c3d4e5f6g7h8'
});
const navItemsStore = useNavItemsStore();
const currentRoute = useRoute();
const currentActiveMenu = computed(() => {
const items = navItemsStore.getNavItems;
if (!Array.isArray(items) || items.length === 0) {
return "";
}
// Cari item utama yang sedang aktif
const currentItem = items.find((item) => item.path === currentRoute.path);
if (currentItem) {
return currentItem.name;
}
// Cari item anak (children) yang sedang aktif
for (const item of items) {
if (item.children && Array.isArray(item.children)) {
const childItem = item.children.find(
(child) => child.path === currentRoute.path
);
if (childItem) {
return item.name;
}
}
}
return "";
const props = defineProps({
items: {
type: Array as () => NavItem[],
required: true,
},
rail: {
type: Boolean,
required: true,
},
drawer: {
type: Boolean,
required: true,
},
});
// Hover handlers
const emit = defineEmits(['update:drawer', 'toggle-rail']);
const openedGroups = ref<string[]>([]);
watch(() => props.rail, (newRailState) => {
if (newRailState === true) {
openedGroups.value = [];
}
});
const isHovering = ref(false);
const handleMouseEnter = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
if (props.rail) {
isHovering.value = true;
emit('toggle-rail');
}
hoverTimeout.value = setTimeout(() => {
rail.value = false;
}, 100);
};
const handleMouseLeave = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
if (isHovering.value) {
isHovering.value = false;
emit('toggle-rail');
}
hoverTimeout.value = setTimeout(() => {
rail.value = true;
}, 200);
};
const handleLogout = () => {
console.log("Logging out...");
// Tambahkan logika logout di sini
// --- AUTH LOGIC (Kept the same) ---
const handleLogout = async () => {
console.log('🚪 SideBar logout initiated...');
try {
await logout();
} catch (error) {
console.error('❌ SideBar logout error:', error);
}
};
const redirectToLogin = () => {
navigateTo('/LoginPage');
};
onMounted(async () => {
await checkAuth();
});
</script>
<style scoped>
.v-list-item--active {
background-color: #e3f2fd !important;
color: #1976d2 !important;
}
.v-list-item--active :deep(.v-list-item__prepend) {
color: #1976d2 !important;
}
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
.v-navigation-drawer {
border-right: 1px solid #e0e0e0 !important;
transition: width 0.2s ease-in-out !important;
}
.hover-bg:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
/* Custom scrollbar */
:deep(.v-navigation-drawer__content) {
overflow-y: auto;
overflow-x: hidden;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar {
width: 6px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-track {
background: transparent;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb:hover {
background: #999;
}
/* No specific overrides needed */
</style>
+181
View File
@@ -0,0 +1,181 @@
// composables/useQueue.js
import { ref, computed } from 'vue';
import { useQueueStore } from '../stores/queueStore';
export const useQueue = (adminType = 'loket') => {
const queueStore = useQueueStore();
// Local state
const snackbar = ref(false);
const snackbarText = ref('');
const snackbarColor = ref('success');
// Dialog states
const showKlinikDialog = ref(false);
const showPenunjangDialog = ref(false);
const showChangeKlinikDialog = ref(false);
const klinikSearch = ref('');
const penunjangSearch = ref('');
const changeKlinikSearch = ref('');
const selectedPatientForPenunjang = ref(null);
// Computed from store - filtered by stage
const currentProcessingPatient = queueStore.getCurrentProcessing(adminType);
// Get patients based on admin type stage - ISOLATED PER ADMIN
const stagePatients = computed(() => {
return queueStore.getPatientsByStage(adminType).value;
});
// Tabel "Di Loket" - hanya untuk stage admin ini
const diLoketPatients = computed(() => stagePatients.value.diLoket);
// Tabel "Terlambat" - hanya untuk stage admin ini
const terlambatPatients = computed(() => stagePatients.value.terlambat);
// Tabel "Pending" - hanya untuk stage admin ini
const pendingPatients = computed(() => stagePatients.value.pending);
// Pasien waiting - hanya untuk stage admin ini
const waitingPatients = computed(() => stagePatients.value.waiting);
const nextPatient = computed(() => {
return waitingPatients.value[0] || null;
});
// Total pasien hanya untuk stage admin ini
const totalPasien = computed(() =>
queueStore.getTotalPasienByStage(adminType).value
);
const quotaUsed = computed(() => queueStore.quotaUsed);
// Filtered lists
const filteredKliniks = computed(() => {
if (!klinikSearch.value) return queueStore.kliniks;
return queueStore.kliniks.filter((k) =>
k.name.toLowerCase().includes(klinikSearch.value.toLowerCase())
);
});
const filteredPenunjangs = computed(() => {
if (!penunjangSearch.value) return queueStore.penunjangs;
return queueStore.penunjangs.filter((p) =>
p.name.toLowerCase().includes(penunjangSearch.value.toLowerCase())
);
});
const filteredChangeKliniks = computed(() => {
if (!changeKlinikSearch.value) return queueStore.kliniks;
return queueStore.kliniks.filter((k) =>
k.name.toLowerCase().includes(changeKlinikSearch.value.toLowerCase())
);
});
// Methods
const showSnackbar = (text, color = 'success') => {
snackbarText.value = text;
snackbarColor.value = color;
snackbar.value = true;
};
const callNext = () => {
const result = queueStore.callNext(adminType);
showSnackbar(result.message, result.success ? 'success' : 'warning');
};
const callMultiplePatients = (count) => {
const result = queueStore.callMultiplePatients(count, adminType);
showSnackbar(result.message, result.success ? 'success' : 'warning');
};
const processPatient = (patient, action) => {
const result = queueStore.processPatient(patient, action, adminType);
let color = 'success';
if (action === 'terlambat') color = 'warning';
else if (action === 'pending') color = 'info';
showSnackbar(result.message, color);
};
const selectKlinik = (klinik) => {
const result = queueStore.createAntreanKlinik(klinik, adminType);
showSnackbar(result.message, 'success');
showKlinikDialog.value = false;
};
const selectPenunjang = (penunjang) => {
const result = queueStore.createAntreanPenunjang(
penunjang,
selectedPatientForPenunjang.value,
adminType
);
showSnackbar(result.message, 'success');
showPenunjangDialog.value = false;
selectedPatientForPenunjang.value = null;
};
const openPenunjangDialog = (patient = null) => {
selectedPatientForPenunjang.value = patient;
showPenunjangDialog.value = true;
};
const changeKlinik = (klinik) => {
if (currentProcessingPatient.value) {
const result = queueStore.changeKlinik(
currentProcessingPatient.value,
klinik,
adminType
);
showSnackbar(result.message, result.success ? 'success' : 'error');
showChangeKlinikDialog.value = false;
}
};
const getRowClass = (item) => {
if (item.status === 'current') {
return 'text-success font-weight-bold';
}
return '';
};
return {
// State
snackbar,
snackbarText,
snackbarColor,
showKlinikDialog,
showPenunjangDialog,
showChangeKlinikDialog,
klinikSearch,
penunjangSearch,
changeKlinikSearch,
selectedPatientForPenunjang,
// Computed
currentProcessingPatient,
diLoketPatients,
terlambatPatients,
pendingPatients,
waitingPatients,
nextPatient,
totalPasien,
quotaUsed,
filteredKliniks,
filteredPenunjangs,
filteredChangeKliniks,
stagePatients,
// Methods
showSnackbar,
callNext,
callMultiplePatients,
processPatient,
selectKlinik,
selectPenunjang,
openPenunjangDialog,
changeKlinik,
getRowClass,
};
};
+1 -1
View File
@@ -15,7 +15,7 @@ export default defineNuxtConfig({
'@nuxt/test-utils',
'@nuxt/ui',
'@pinia/nuxt',
'nuxt-qrcode',
// Remove '@sidebase/nuxt-auth' completely
(_options, nuxt) => {
nuxt.hooks.hook('vite:extendConfig', (config) => {
+703 -329
View File
File diff suppressed because it is too large Load diff
+269 -353
View File
@@ -23,31 +23,7 @@
</div>
</div>
</div>
<!-- <v-card class="next-patient-card mb-4" elevation="2">
<v-card-text class="text-center pa-6">
<v-chip color="success" variant="flat" class="mb-3" size="large">
<v-icon start>mdi-account-arrow-right</v-icon>
PASIEN ANTREAN ANJUNGAN
</v-chip>
<div class="text-h4 font-weight-bold mb-2 text-success">
{{ nextPatient ? nextPatient.noAntrian.split(" |")[0] : "UM1004" }}
</div>
<div class="text-body-1 mb-4 text-grey-darken-1">
Klik tombol hijau untuk memanggil
</div>
<v-btn
color="success"
variant="flat"
size="large"
class="px-8"
@click="callNext"
:disabled="!nextPatient"
>
<v-icon start>mdi-microphone</v-icon>
PANGGIL NEXT
</v-btn>
</v-card-text>
</v-card> -->
<!-- Combined Control Section -->
<v-row class="mb-4">
<!-- Left Side: Current Processing -->
@@ -72,41 +48,66 @@
{{ currentProcessingPatient.barcode }} |
{{ currentProcessingPatient.klinik }}
</div>
<div class="patient-meta">
Jenis Pembayaran: {{ currentProcessingPatient.pembayaran }}
</div>
</div>
<div class="action-buttons d-flex flex-column">
<v-btn
color="success"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
<v-btn
color="error"
variant="flat"
width="120" @click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Ubah Klinik
</v-btn>
</div>
<div class="action-buttons d-flex flex-column">
<v-btn
color="success"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
<v-btn
color="info"
variant="flat"
width="120"
@click="showChangeKlinikDialog = true"
>
<v-icon start>mdi-swap-horizontal</v-icon>
Ubah Klinik
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
<!-- Empty State -->
<v-card v-else class="current-processing-card" elevation="2">
<v-card-text class="pa-6 text-center">
<v-icon size="64" color="grey-lighten-1" class="mb-3">
mdi-account-off-outline
</v-icon>
<div class="text-h6 text-grey-darken-1 mb-2">
Tidak Ada Pasien yang Diproses
</div>
<div class="text-body-2 text-grey">
Pilih pasien dari tabel "Di Loket" untuk memproses
</div>
</v-card-text>
</v-card>
@@ -114,22 +115,15 @@
<!-- Right Side: Queue Info and Call Controls -->
<v-col cols="12" lg="6">
<!-- Quota Info and Call Controls Combined Card -->
<v-card class="quota-call-card" elevation="2">
<v-card-text class="pa-4">
<!-- Header with icon -->
<div class="d-flex align-center justify-center mb-3">
<v-chip
color="success"
variant="flat"
size="large"
>
<v-chip color="success" variant="flat" size="large">
<v-icon start>mdi-account-arrow-right</v-icon>
PANGGIL ANTREAN ANJUNGAN
</v-chip>
</div>
<!-- Quota Display -->
<v-row class="mb-3">
<v-col cols="6" class="text-center">
<div class="quota-label">Kuota</div>
@@ -143,7 +137,6 @@
</v-col>
</v-row>
<!-- Progress Bar -->
<div class="text-center mb-3">
<div class="quota-used-text">
Total Quota Terpakai: {{ quotaUsed }}
@@ -157,10 +150,8 @@
></v-progress-linear>
</div>
<!-- Divider -->
<v-divider class="my-3"></v-divider>
<!-- Call Controls -->
<div class="call-controls">
<div class="call-label mb-2">Kumulatif Panggil Antrean</div>
<div class="call-buttons">
@@ -168,7 +159,7 @@
color="success"
variant="flat"
class="call-btn"
@click="callMultiplePatients(1)"
@click="callNext"
:disabled="!nextPatient"
>
<span class="call-btn-text">1</span>
@@ -204,6 +195,31 @@
</v-col>
</v-row>
<v-row class="mb-4">
<v-col cols="12">
<div class="d-flex justify-center gap-2">
<v-btn
color="primary"
variant="flat"
size="large"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik Ruang
</v-btn>
<v-btn
color="secondary"
variant="flat"
size="large"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</div>
</v-col>
</v-row>
<!-- Di Loket Patients Table -->
<v-card class="main-table-card mb-4" elevation="2">
<TabelData
@@ -231,9 +247,6 @@
</v-btn>
</div>
</template>
<template #item.jamPanggil="{ item }">
<span :class="getRowClass(item)">{{ item.jamPanggil }}</span>
</template>
</TabelData>
</v-card>
@@ -289,6 +302,149 @@
</TabelData>
</v-card>
<!-- Klinik Dialog -->
<v-dialog v-model="showKlinikDialog" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center pa-4 bg-primary text-white">
<v-btn icon @click="showKlinikDialog = false" class="mr-2">
<v-icon color="white">mdi-arrow-left</v-icon>
</v-btn>
<span class="text-h6">Buat Antrean Klinik Ruang</span>
</v-card-title>
<v-card-subtitle class="pa-4 text-center">
<h3 class="text-h6 mb-3">Pilih Klinik</h3>
<div class="d-flex justify-end gap-2">
<v-text-field
v-model="klinikSearch"
placeholder="Pencarian"
density="compact"
hide-details
style="max-width: 200px"
></v-text-field>
</div>
</v-card-subtitle>
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="klinik in filteredKliniks"
:key="klinik.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="klinik-card"
elevation="1"
@click="selectKlinik(klinik)"
>
<v-card-text class="text-center pa-4">
<div class="text-h6 font-weight-bold">{{ klinik.name }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Penunjang Dialog -->
<v-dialog v-model="showPenunjangDialog" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center pa-4 bg-secondary text-white">
<v-btn icon @click="showPenunjangDialog = false" class="mr-2">
<v-icon color="white">mdi-arrow-left</v-icon>
</v-btn>
<span class="text-h6">Buat Antrean Penunjang</span>
</v-card-title>
<v-card-subtitle class="pa-4">
<h3 class="text-h6 mb-3 text-center">Pilih Ruang</h3>
<div class="d-flex justify-end gap-2">
<v-text-field
v-model="penunjangSearch"
placeholder="Pencarian"
density="compact"
hide-details
style="max-width: 200px"
></v-text-field>
</div>
</v-card-subtitle>
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="penunjang in filteredPenunjangs"
:key="penunjang.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="penunjang-card"
elevation="1"
@click="selectPenunjang(penunjang)"
>
<v-card-text class="text-center pa-4">
<div class="text-h6 font-weight-bold">
{{ penunjang.name }}
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Change Klinik Dialog -->
<v-dialog v-model="showChangeKlinikDialog" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center pa-4 bg-info text-white">
<v-btn icon @click="showChangeKlinikDialog = false" class="mr-2">
<v-icon color="white">mdi-arrow-left</v-icon>
</v-btn>
<span class="text-h6">Ubah Klinik</span>
</v-card-title>
<v-card-subtitle class="pa-4">
<h3 class="text-h6 mb-3 text-center">Pilih Klinik Baru</h3>
<div class="d-flex justify-end gap-2">
<v-text-field
v-model="changeKlinikSearch"
placeholder="Pencarian"
density="compact"
hide-details
style="max-width: 200px"
></v-text-field>
</div>
</v-card-subtitle>
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="klinik in filteredChangeKliniks"
:key="klinik.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="klinik-card"
elevation="1"
@click="changeKlinik(klinik)"
>
<v-card-text class="text-center pa-4">
<div class="text-h6 font-weight-bold">{{ klinik.name }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar
v-model="snackbar"
@@ -297,7 +453,6 @@
location="top right"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon @click="snackbar = false">
<v-icon>mdi-close</v-icon>
@@ -308,99 +463,39 @@
</template>
<script setup>
import { ref, computed } from "vue";
import { ref } from "vue";
import TabelData from "../components/TabelData.vue";
import { useQueue } from "../composables/useQueue";
// Reactive data
const snackbar = ref(false);
const snackbarText = ref("");
const snackbarColor = ref("success");
const quotaUsed = ref(5);
const currentProcessingPatient = ref(null);
// Base patient data - semua pasien yang belum dipanggil
const allPatients = ref([
{
no: 1,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
{
no: 3,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1003 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
{
no: 4,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1004 | Online - 250811100210",
shift: "Shift 1",
klinik: "THT",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
...Array.from({ length: 16 }, (_, i) => ({
no: i + 5,
jamPanggil: `${String(Math.floor(Math.random() * 12) + 1).padStart(
2,
"0"
)}:${String(Math.floor(Math.random() * 60)).padStart(2, "0")}`,
barcode: `25081110${String(i + 300).padStart(4, "0")}`,
noAntrian: `UM100${i + 5} | Online - 25081110${String(i + 300).padStart(
4,
"0"
)}`,
shift: "Shift 1",
klinik: ["KANDUNGAN", "IPD", "THT", "SARAF"][Math.floor(Math.random() * 4)],
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
})),
]);
// Computed properties for different status tables
const diLoketPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "di-loket")
);
const terlambatPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "terlambat")
);
const pendingPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "pending")
);
const nextPatient = computed(() => {
return allPatients.value.find((patient) => patient.status === "waiting");
});
const totalPasien = computed(() => allPatients.value.length);
// Use queue composable with 'loket' admin type
const {
snackbar,
snackbarText,
snackbarColor,
showKlinikDialog,
showPenunjangDialog,
showChangeKlinikDialog,
klinikSearch,
penunjangSearch,
changeKlinikSearch,
currentProcessingPatient,
diLoketPatients,
terlambatPatients,
pendingPatients,
nextPatient,
totalPasien,
quotaUsed,
filteredKliniks,
filteredPenunjangs,
filteredChangeKliniks,
callNext,
callMultiplePatients,
processPatient,
selectKlinik,
selectPenunjang,
openPenunjangDialog,
changeKlinik,
} = useQueue('loket');
// Headers for different tables
const diLoketHeaders = ref([
@@ -434,107 +529,6 @@ const pendingHeaders = ref([
{ title: "Pembayaran", value: "pembayaran", sortable: true, width: "100px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "100px" },
]);
// Methods
const showSnackbar = (text, color = "success") => {
snackbarText.value = text;
snackbarColor.value = color;
snackbar.value = true;
};
const callMultiplePatients = (count) => {
const waitingPatients = allPatients.value.filter(
(patient) => patient.status === "waiting"
);
const patientsToCall = waitingPatients.slice(0, count);
if (patientsToCall.length === 0) {
showSnackbar("Tidak ada pasien yang menunggu", "warning");
return;
}
if (quotaUsed.value + patientsToCall.length > 150) {
showSnackbar("Quota tidak mencukupi", "error");
return;
}
patientsToCall.forEach((patient) => {
patient.status = "di-loket";
});
quotaUsed.value += patientsToCall.length;
showSnackbar(`Memanggil ${patientsToCall.length} pasien ke loket`, "success");
};
const callNext = () => {
if (!nextPatient.value) {
showSnackbar("Tidak ada pasien selanjutnya", "warning");
return;
}
if (quotaUsed.value >= 150) {
showSnackbar("Quota sudah penuh", "error");
return;
}
nextPatient.value.status = "di-loket";
currentProcessingPatient.value = nextPatient.value;
quotaUsed.value++;
showSnackbar(
`Memanggil pasien ${nextPatient.value.noAntrian.split(" |")[0]}`,
"success"
);
};
const processPatient = (patient, action) => {
const patientCode = patient.noAntrian.split(" |")[0];
switch (action) {
case "check-in":
patient.status = "processed";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} berhasil check in`, "success");
break;
case "terlambat":
patient.status = "terlambat";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} ditandai terlambat`, "warning");
break;
case "pending":
patient.status = "pending";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} di-pending`, "info");
break;
case "aktifkan":
if (patient.status === "terlambat") {
patient.status = "di-loket";
showSnackbar(`Pasien ${patientCode} diaktifkan kembali`, "success");
}
break;
case "proses":
currentProcessingPatient.value = patient;
showSnackbar(`Memproses pasien ${patientCode}`, "info");
break;
}
};
const getRowClass = (item) => {
if (item.status === "current") {
return "text-success font-weight-bold";
}
return "";
};
</script>
<style scoped>
@@ -618,7 +612,6 @@ const getRowClass = (item) => {
align-items: center;
}
/* Quota Display Styling */
.quota-label {
font-size: 13px;
color: #6b7280;
@@ -643,7 +636,6 @@ const getRowClass = (item) => {
font-weight: 500;
}
/* Call Controls Styling */
.call-controls {
text-align: center;
}
@@ -673,31 +665,18 @@ const getRowClass = (item) => {
font-weight: 700;
}
/* Enhanced table styling */
.main-table-card :deep(.v-data-table th),
.late-table-card :deep(.v-data-table th),
.pending-table-card :deep(.v-data-table th) {
background: #fafbfc;
font-weight: 600;
font-size: 13px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
.klinik-card,
.penunjang-card {
cursor: pointer;
transition: all 0.3s ease;
}
.main-table-card :deep(.v-data-table tbody tr:hover),
.late-table-card :deep(.v-data-table tbody tr:hover),
.pending-table-card :deep(.v-data-table tbody tr:hover) {
background: #f8fafc !important;
.klinik-card:hover,
.penunjang-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.main-table-card :deep(.v-data-table tbody td),
.late-table-card :deep(.v-data-table tbody td),
.pending-table-card :deep(.v-data-table tbody td) {
padding: 12px 16px;
border-bottom: 1px solid #f1f5f9;
}
/* Button styling */
.v-btn {
text-transform: none !important;
}
@@ -707,12 +686,10 @@ const getRowClass = (item) => {
padding: 0 12px;
}
/* Success text color */
.text-success {
color: #4caf50 !important;
}
/* Responsive Design */
@media (max-width: 1024px) {
.header-content {
flex-direction: column;
@@ -729,12 +706,6 @@ const getRowClass = (item) => {
font-size: 28px;
}
.current-processing-card .d-flex {
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.action-buttons {
flex-direction: row;
flex-wrap: wrap;
@@ -746,15 +717,6 @@ const getRowClass = (item) => {
flex: 1;
min-width: 120px;
}
.call-buttons {
gap: 8px;
}
.call-btn {
min-width: 70px;
height: 44px;
}
}
@media (max-width: 768px) {
@@ -770,25 +732,6 @@ const getRowClass = (item) => {
font-size: 24px;
}
.header-icon {
padding: 12px;
}
.call-buttons {
flex-direction: row;
gap: 8px;
}
.call-btn {
flex: 1;
min-width: 60px;
height: 42px;
}
.call-btn-text {
font-size: 16px;
}
.action-buttons {
flex-direction: column;
width: 100%;
@@ -803,31 +746,4 @@ const getRowClass = (item) => {
font-size: 28px;
}
}
@media (max-width: 600px) {
.page-title {
font-size: 20px;
}
.current-processing-card .patient-info .text-h5 {
font-size: 1.25rem !important;
}
.quota-number {
font-size: 24px;
}
.call-buttons {
flex-wrap: wrap;
}
.call-btn {
min-width: calc(50% - 8px);
height: 40px;
}
.call-btn-text {
font-size: 14px;
}
}
</style>
+694
View File
@@ -0,0 +1,694 @@
<!-- pages/AdminPenunjang.vue -->
<template>
<div class="loket-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-pulse</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Penunjang Admin</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
<div class="header-right">
<v-chip color="success" variant="flat" class="mr-2">
Total {{ totalPasien }} Pasien
</v-chip>
<v-chip color="white" variant="flat" class="text-primary">
Max 150 Pasien
</v-chip>
</div>
</div>
</div>
<!-- Combined Control Section -->
<v-row class="mb-4">
<!-- Left Side: Current Processing -->
<v-col cols="12" lg="6">
<!-- Current Patient Processing Card -->
<v-card class="current-processing-card" elevation="2">
<v-card-text class="pa-4">
<!-- State when there's a patient being processed -->
<div
v-if="currentProcessingPatient"
class="d-flex align-center justify-space-between"
>
<div class="patient-info">
<v-chip color="primary" variant="flat" class="mb-2">
<v-icon start>mdi-account-clock</v-icon>
SEDANG DIPROSES
</v-chip>
<div class="text-h5 font-weight-bold mb-1">
{{ currentProcessingPatient.noAntrian.split(" |")[0] }}
</div>
<div class="text-subtitle-1 text-grey-darken-1">
{{ currentProcessingPatient.barcode }} |
{{ currentProcessingPatient.klinik }}
</div>
<div class="patient-meta">
Jenis Pembayaran: {{ currentProcessingPatient.pembayaran }}
</div>
</div>
<div class="action-buttons d-flex flex-column">
<v-btn
color="success"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
class="mb-2"
width="120"
@click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
<v-btn
color="error"
variant="flat"
width="120"
@click="showChangeKlinikDialog = true"
>
<v-icon start>mdi-swap-horizontal</v-icon>
Ubah Klinik
</v-btn>
</div>
</div>
<!-- Empty state when no patient is being processed -->
<div v-else class="text-center pa-6">
<v-icon size="64" color="grey-lighten-1" class="mb-3">
mdi-account-off-outline
</v-icon>
<div class="text-h6 text-grey-darken-1 mb-2">
Tidak Ada Pasien yang Diproses
</div>
<div class="text-body-2 text-grey">
Pilih pasien dari tabel "Di Loket" untuk memproses
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Right Side: Queue Info and Call Controls -->
<v-col cols="12" lg="6">
<v-card class="quota-call-card" elevation="2">
<v-card-text class="pa-4">
<div class="d-flex align-center justify-center mb-3">
<v-chip color="success" variant="flat" size="large">
<v-icon start>mdi-account-arrow-right</v-icon>
PANGGIL ANTREAN ANJUNGAN
</v-chip>
</div>
<v-row class="mb-3">
<v-col cols="6" class="text-center">
<div class="quota-label">Kuota</div>
<div class="quota-number">150</div>
</v-col>
<v-col cols="6" class="text-center">
<div class="quota-label">Tersedia</div>
<div class="quota-number quota-available">
{{ 150 - quotaUsed }}
</div>
</v-col>
</v-row>
<div class="text-center mb-3">
<div class="quota-used-text">
Total Quota Terpakai: {{ quotaUsed }}
</div>
<v-progress-linear
:model-value="(quotaUsed / 150) * 100"
color="success"
height="6"
rounded
class="mt-2"
></v-progress-linear>
</div>
<v-divider class="my-3"></v-divider>
<div class="call-controls">
<div class="call-label mb-2">Kumulatif Panggil Antrean</div>
<div class="call-buttons">
<v-btn
color="success"
variant="flat"
class="call-btn"
@click="callNext"
:disabled="!nextPatient"
>
<span class="call-btn-text">1</span>
</v-btn>
<v-btn
color="info"
variant="flat"
class="call-btn"
@click="callMultiplePatients(5)"
>
<span class="call-btn-text">5</span>
</v-btn>
<v-btn
color="warning"
variant="flat"
class="call-btn"
@click="callMultiplePatients(10)"
>
<span class="call-btn-text">10</span>
</v-btn>
<v-btn
color="error"
variant="flat"
class="call-btn"
@click="callMultiplePatients(20)"
>
<span class="call-btn-text">20</span>
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mb-4">
<v-col cols="12">
<div class="d-flex justify-center">
<v-btn
color="secondary"
variant="flat"
size="large"
@click="showPenunjangDialog = true"
class="mx-2"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</div>
</v-col>
</v-row>
<!-- Di Loket Patients Table -->
<v-card class="main-table-card mb-4" elevation="2">
<TabelData
:headers="diLoketHeaders"
:items="diLoketPatients"
title="DATA PASIEN - DI LOKET"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="info"
variant="flat"
@click="processPatient(item, 'aktifkan')"
>
Aktifkan
</v-btn>
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
</div>
</template>
</TabelData>
</v-card>
<!-- Terlambat Patients Table -->
<v-card
class="late-table-card mb-4"
elevation="2"
v-if="terlambatPatients.length > 0"
>
<TabelData
:headers="terlambatHeaders"
:items="terlambatPatients"
title="INFO PASIEN LAPOR TERLAMBAT"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'aktifkan')"
>
Aktifkan
</v-btn>
</div>
</template>
</TabelData>
</v-card>
<!-- Pending Patients Table -->
<v-card
class="pending-table-card mb-4"
elevation="2"
v-if="pendingPatients.length > 0"
>
<TabelData
:headers="pendingHeaders"
:items="pendingPatients"
title="INFO PASIEN PENDING"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
</div>
</template>
</TabelData>
</v-card>
<!-- Penunjang Dialog -->
<v-dialog v-model="showPenunjangDialog" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center pa-4 bg-secondary text-white">
<v-btn icon @click="showPenunjangDialog = false" class="mr-2">
<v-icon color="white">mdi-arrow-left</v-icon>
</v-btn>
<span class="text-h6">Buat Antrean Penunjang</span>
</v-card-title>
<v-card-subtitle class="pa-4">
<h3 class="text-h6 mb-3 text-center">Pilih Ruang</h3>
<div class="d-flex justify-end gap-2">
<v-text-field
v-model="penunjangSearch"
placeholder="Pencarian"
density="compact"
hide-details
style="max-width: 200px"
></v-text-field>
</div>
</v-card-subtitle>
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="penunjang in filteredPenunjangs"
:key="penunjang.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="penunjang-card"
elevation="1"
@click="selectPenunjang(penunjang)"
>
<v-card-text class="text-center pa-4">
<div class="text-h6 font-weight-bold">
{{ penunjang.name }}
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Change Klinik Dialog -->
<v-dialog v-model="showChangeKlinikDialog" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center pa-4 bg-info text-white">
<v-btn icon @click="showChangeKlinikDialog = false" class="mr-2">
<v-icon color="white">mdi-arrow-left</v-icon>
</v-btn>
<span class="text-h6">Ubah Klinik</span>
</v-card-title>
<v-card-subtitle class="pa-4">
<h3 class="text-h6 mb-3 text-center">Pilih Klinik Baru</h3>
<div class="d-flex justify-end gap-2">
<v-text-field
v-model="changeKlinikSearch"
placeholder="Pencarian"
density="compact"
hide-details
style="max-width: 200px"
></v-text-field>
</div>
</v-card-subtitle>
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="klinik in filteredChangeKliniks"
:key="klinik.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="klinik-card"
elevation="1"
@click="changeKlinik(klinik)"
>
<v-card-text class="text-center pa-4">
<div class="text-h6 font-weight-bold">{{ klinik.name }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
location="top right"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon @click="snackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref } from "vue";
import TabelData from "../components/TabelData.vue";
import { useQueue } from "../composables/useQueue";
// Use queue composable with 'penunjang' admin type
const {
snackbar,
snackbarText,
snackbarColor,
showPenunjangDialog,
showChangeKlinikDialog,
penunjangSearch,
changeKlinikSearch,
currentProcessingPatient,
diLoketPatients,
terlambatPatients,
pendingPatients,
nextPatient,
totalPasien,
quotaUsed,
filteredPenunjangs,
filteredChangeKliniks,
callNext,
callMultiplePatients,
processPatient,
selectPenunjang,
changeKlinik,
} = useQueue('penunjang');
const diLoketHeaders = ref([
{ title: "No", value: "no", sortable: false, width: "60px" },
{ title: "Jam Panggil", value: "jamPanggil", sortable: true, width: "100px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Fast Track", value: "fastTrack", sortable: true, width: "100px" },
{ title: "Pembayaran", value: "pembayaran", sortable: true, width: "100px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "200px" },
]);
const terlambatHeaders = ref([
{ title: "No", value: "no", sortable: false, width: "60px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "100px" },
]);
const pendingHeaders = ref([
{ title: "#", value: "no", sortable: false, width: "60px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Fast Track", value: "fastTrack", sortable: true, width: "100px" },
{ title: "Pembayaran", value: "pembayaran", sortable: true, width: "100px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "100px" },
]);
</script>
<style scoped>
.loket-container {
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.page-header {
background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(156, 39, 176, 0.3);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: white;
}
.header-left {
display: flex;
align-items: center;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.page-title {
font-size: 32px;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
.header-right {
display: flex;
align-items: center;
}
.current-processing-card,
.quota-call-card,
.main-table-card,
.late-table-card,
.pending-table-card {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.current-processing-card {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 2px solid rgba(255, 152, 0, 0.2);
}
.quota-call-card {
background: linear-gradient(135deg, #e3f2fd 0%, #f5f5f5 100%);
border: 2px solid rgba(33, 150, 243, 0.2);
}
.patient-info .text-h5 {
color: #1976d2;
}
.action-buttons {
display: flex;
align-items: center;
}
.quota-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
margin-bottom: 4px;
}
.quota-number {
font-size: 32px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.quota-available {
color: #4caf50;
}
.quota-used-text {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.call-controls {
text-align: center;
}
.call-label {
font-size: 14px;
font-weight: 600;
color: #374151;
text-align: center;
}
.call-buttons {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.call-btn {
min-width: 80px;
height: 48px;
border-radius: 8px;
}
.call-btn-text {
font-size: 18px;
font-weight: 700;
}
.v-btn {
text-transform: none !important;
}
.v-btn--size-small {
height: 32px;
padding: 0 12px;
}
.text-success {
color: #4caf50 !important;
}
.penunjang-card {
cursor: pointer;
transition: all 0.3s ease;
}
.penunjang-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.klinik-card {
cursor: pointer;
transition: all 0.3s ease;
}
.klinik-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
@media (max-width: 1024px) {
.header-content {
flex-direction: column;
gap: 20px;
text-align: center;
}
.header-left {
flex-direction: column;
gap: 12px;
}
.page-title {
font-size: 28px;
}
.action-buttons {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.action-buttons .v-btn {
flex: 1;
min-width: 120px;
}
}
@media (max-width: 768px) {
.loket-container {
padding: 16px;
}
.header-content {
padding: 24px 20px;
}
.page-title {
font-size: 24px;
}
.action-buttons {
flex-direction: column;
width: 100%;
}
.action-buttons .v-btn {
width: 100%;
margin: 4px 0;
}
.quota-number {
font-size: 28px;
}
}
</style>
+312 -335
View File
@@ -1,7 +1,5 @@
<!-- pages/Anjungan.vue -->
<template>
<div class="anjungan-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
@@ -22,60 +20,7 @@
</div>
</div>
<!-- Controls Section -->
<!-- <v-card class="controls-card mb-4" elevation="2">
<v-card-text class="py-3">
<v-row align="center">
<v-col cols="12" md="6">
<div class="d-flex align-center flex-wrap gap-3">
<v-select
v-model="selectedStatus"
:items="statusOptions"
label="Filter Status"
density="compact"
variant="outlined"
hide-details
clearable
style="min-width: 160px;"
/>
<v-text-field
v-model="searchQuery"
label="Cari Klinik"
density="compact"
variant="outlined"
prepend-inner-icon="mdi-magnify"
hide-details
clearable
style="min-width: 200px;"
/>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="d-flex justify-end align-center flex-wrap gap-2">
<v-btn
variant="outlined"
prepend-icon="mdi-refresh"
@click="refreshData"
:loading="loading"
size="small"
>
Refresh
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card> -->
<!-- Clinic Cards -->
<v-card elevation="2" class="main-content-card">
<!-- <v-card-title class="d-flex align-center pa-4 bg-grey-lighten-4">
<v-icon class="mr-2">mdi-hospital-marker</v-icon>
<span>Daftar Klinik - {{ filteredClinics.length }} dari {{ totalClinics }} klinik</span>
</v-card-title> -->
<v-card-text class="pa-6">
<v-row>
<v-col
@@ -87,7 +32,7 @@
lg="3"
class="pa-2"
>
<v-card
<v-card
:class="{
'clinic-card': true,
'clinic-available': clinic.available,
@@ -98,19 +43,25 @@
elevation="2"
>
<v-card-text class="text-center pa-4">
<!-- Status Chip -->
<div class="mb-3">
<div class="mb-6">
<v-chip
:color="clinic.available ? 'success' : 'error'"
size="small"
variant="flat"
class="mr-2"
>
{{ clinic.available ? 'TERSEDIA' : 'TUTUP' }}
</v-chip>
<v-chip
size="small"
:color="clinic.available ? 'info' : 'error'"
variant="outlined"
>
{{ clinic.shift }}
</v-chip>
</div>
<!-- Icon -->
<div class="clinic-icon-wrapper mb-3">
<div class="clinic-icon-wrapper mb-6">
<v-icon
:icon="clinic.icon"
size="40"
@@ -118,60 +69,24 @@
></v-icon>
</div>
<!-- Clinic Name -->
<h3 class="text-h6 font-weight-bold mb-2">
{{ clinic.name }}
</h3>
<!-- Subtitle -->
<p v-if="clinic.subtitle" class="text-caption text-grey-darken-1 mb-2">
{{ clinic.subtitle }}
</p>
<!-- Shift Info -->
<div class="shift-info">
<v-chip
size="small"
:color="clinic.available ? 'info' : 'error'"
variant="outlined"
class="mb-2"
>
{{ clinic.shift }}
</v-chip>
<br>
<span v-if="clinic.schedule" class="text-caption text-grey-darken-1">
{{ clinic.schedule }}
</span>
</div>
<!-- Action Button -->
<div class="mt-3">
<v-btn
v-if="clinic.available"
color="success"
variant="flat"
size="small"
block
>
Pilih Klinik
</v-btn>
<v-btn
v-else
color="error"
variant="outlined"
size="small"
disabled
block
>
Tidak Tersedia
</v-btn>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Empty State -->
<div v-if="filteredClinics.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-hospital-marker-outline</v-icon>
<h3 class="text-h6 mt-4 text-grey-darken-1">Tidak ada klinik yang sesuai filter</h3>
@@ -180,9 +95,8 @@
</v-card-text>
</v-card>
<!-- Selection Dialog -->
<v-dialog v-model="showDialog" max-width="500">
<v-card>
<v-dialog v-model="showVisitTypeDialog" max-width="400" persistent>
<v-card class="text-center" :style="{ borderRadius: '16px' }">
<v-card-title class="d-flex align-center bg-primary text-white">
<v-icon class="mr-2">mdi-check-circle</v-icon>
Konfirmasi Pilihan
@@ -207,24 +121,191 @@
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn @click="showDialog = false" variant="text">
Batal
</v-btn>
<v-btn
color="primary"
@click="proceedToRegistration"
variant="flat"
<v-divider />
<v-card-text class="pa-4">
<v-btn
color="primary"
class="mb-3"
size="large"
block
@click="selectVisitType('SEKARANG')"
>
Lanjutkan
SEKARANG
</v-btn>
<v-btn
color="warning"
class="mb-3"
size="large"
block
@click="selectVisitType('PESAN')"
>
PESAN
</v-btn>
<v-btn
color="error"
size="large"
block
@click="selectVisitType('FAST TRACK')"
>
FAST TRACK
</v-btn>
</v-card-text>
<v-card-actions class="justify-center pa-4 pt-0">
<v-btn variant="outlined" color="grey-darken-1" @click="showVisitTypeDialog = false">
Tutup
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPaymentTypeDialog" max-width="400" persistent>
<v-card class="text-center" :style="{ borderRadius: '16px' }">
<v-card-title class="headline pa-4">Jenis Pembayaran</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<v-btn
color="success"
class="mb-3 text-h6"
size="x-large"
block
@click="selectPaymentType('BPJS')"
:style="{ height: '80px', fontWeight: 'bold' }"
>
BPJS
</v-btn>
<v-btn
color="blue-grey-darken-2"
class="text-h6"
size="x-large"
block
@click="selectPaymentType('UMUM')"
:style="{ height: '80px', fontWeight: 'bold' }"
>
UMUM / JKMM / SPM / DLL
</v-btn>
</v-card-text>
<v-card-actions class="justify-center pa-4 pt-0">
<v-btn variant="outlined" color="grey-darken-1" @click="showPaymentTypeDialog = false">
Tutup
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showBookingFormDialog" max-width="500" persistent>
<v-card :style="{ borderRadius: '16px' }">
<v-card-title class="headline pa-4 bg-primary text-white">
Pemesanan Antrian Poliklinik
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submitBooking">
<v-text-field
v-model="bookingForm.date"
label="Tanggal"
type="date"
variant="outlined"
density="compact"
required
class="mb-3"
></v-text-field>
<v-select
v-model="bookingForm.shift"
:items="['Shift 1', 'Shift 2']"
label="Shift"
variant="outlined"
density="compact"
required
class="mb-3"
></v-select>
<v-select
v-model="bookingForm.payment"
:items="['BPJS', 'UMUM/JKMM/SPM/DLL']"
label="Pembayaran"
variant="outlined"
density="compact"
required
></v-select>
</v-form>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn @click="showBookingFormDialog = false" variant="outlined" color="grey-darken-1">
Tutup
</v-btn>
<v-btn color="success" variant="flat" @click="submitBooking">
Simpan
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showFastTrackFormDialog" max-width="500" persistent>
<v-card :style="{ borderRadius: '16px' }">
<v-card-title class="headline pa-4 bg-primary text-white">
Form Fast Track
</v-card-title>
<v-card-text class="pa-6">
<v-form @submit.prevent="submitFastTrack">
<v-text-field
v-model="fastTrackForm.guarantor"
label="Penanggung Jawab"
variant="outlined"
density="compact"
required
class="mb-3"
></v-text-field>
<v-text-field
v-model="fastTrackForm.noRek"
label="No Rekam Medis (Jika Ada)"
variant="outlined"
density="compact"
class="mb-3"
></v-text-field>
<v-text-field
v-model="fastTrackForm.patientName"
label="Nama Pasien"
variant="outlined"
density="compact"
required
class="mb-3"
></v-text-field>
<v-textarea
v-model="fastTrackForm.reason"
label="Alasan Fast Track"
variant="outlined"
density="compact"
required
rows="3"
class="mb-3"
></v-textarea>
<v-select
v-model="fastTrackForm.payment"
:items="['BPJS', 'UMUM/JKMM/SPM/DLL']"
label="Pembayaran"
variant="outlined"
density="compact"
required
></v-select>
</v-form>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn @click="showFastTrackFormDialog = false" variant="outlined" color="grey-darken-1">
Tutup
</v-btn>
<v-btn color="success" variant="flat" @click="submitFastTrack">
Simpan
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
@@ -232,7 +313,6 @@
location="top right"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon @click="snackbar = false">
<v-icon>mdi-close</v-icon>
@@ -244,235 +324,72 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
definePageMeta({
layout: false, // Disables the layout for this specific page
});
// Reactive data
definePageMeta({
layout: false, // Disables the layout for this specific page
})
// Reactive data (tambahan untuk dialog baru)
const loading = ref(false)
const selectedStatus = ref(null)
const searchQuery = ref('')
const showDialog = ref(false)
const showVisitTypeDialog = ref(false) // Dialog: Pilih Kunjungan
const showPaymentTypeDialog = ref(false) // Dialog: Jenis Pembayaran (Sekarang)
const showBookingFormDialog = ref(false) // Dialog: Form Pesan
const showFastTrackFormDialog = ref(false) // Dialog: Form Fast Track
const selectedClinic = ref(null)
const selectedVisitType = ref(null)
const snackbar = ref(false)
const snackbarText = ref('')
const snackbarColor = ref('success')
// Options
// Form data models
const bookingForm = ref({
date: new Date().toISOString().substring(0, 10),
shift: 'Shift 1',
payment: null,
})
const fastTrackForm = ref({
guarantor: '',
noRek: '',
patientName: '',
reason: '',
payment: null,
})
// Options (Tidak Berubah)
const statusOptions = ['Tersedia', 'Tutup']
// Clinic data
// Clinic data (Tidak Berubah)
const clinics = ref([
{
id: 1,
name: 'ANAK',
subtitle: '',
icon: 'mdi-baby-face',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 2,
name: 'ANESTESI',
subtitle: '',
icon: 'mdi-sleep',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 3,
name: 'BEDAH',
subtitle: '',
icon: 'mdi-medical-bag',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 4,
name: 'GERIATRI',
subtitle: '',
icon: 'mdi-account-supervisor',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 5,
name: 'GIGI DAN MULUT',
subtitle: 'GIGI DAN MULUT',
icon: 'mdi-tooth',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 6,
name: 'GIZI',
subtitle: '',
icon: 'mdi-food-apple',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 7,
name: 'HOM',
subtitle: 'HEMATO ONKOLOGI MEDIS',
icon: 'mdi-water',
shift: 'TUTUP',
schedule: '',
available: false,
},
{
id: 8,
name: 'IPD',
subtitle: 'PENYAKIT DALAM',
icon: 'mdi-hospital',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 9,
name: 'JANTUNG',
subtitle: 'CARDIOLOGI',
icon: 'mdi-heart-pulse',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 10,
name: 'JIWA',
subtitle: '',
icon: 'mdi-brain',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 11,
name: 'KANDUNGAN',
subtitle: '',
icon: 'mdi-human-pregnant',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 12,
name: 'KEMOTERAPI',
subtitle: '',
icon: 'mdi-needle',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 13,
name: 'KOMPLEMENTER',
subtitle: 'NYERI',
icon: 'mdi-leaf',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 14,
name: 'KUL KEL',
subtitle: 'KULIT KELAMIN',
icon: 'mdi-hand-back-right',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 15,
name: 'MATA',
subtitle: '',
icon: 'mdi-eye',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 16,
name: 'MCU',
subtitle: '',
icon: 'mdi-clipboard-check',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 17,
name: 'ONKOLOGI',
subtitle: '',
icon: 'mdi-ribbon',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 18,
name: 'PARU',
subtitle: '',
icon: 'mdi-lungs',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 19,
name: 'R. TINDAKAN',
subtitle: 'EMG, ECG, DLL',
icon: 'mdi-monitor-heart-rate',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 20,
name: 'RADIOTERAPI',
subtitle: '',
icon: 'mdi-radioactive',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 21,
name: 'REHAB MEDIK',
subtitle: '',
icon: 'mdi-human-cane',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 22,
name: 'SARAF',
subtitle: 'NEUROLOGI',
icon: 'mdi-head-cog',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: false,
},
{
id: 23,
name: 'THT',
subtitle: '',
icon: 'mdi-ear-hearing',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
}
// ... data klinik ... (dibiarkan seperti semula)
{ id: 1, name: 'ANAK', subtitle: '', icon: 'mdi-baby-face', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 2, name: 'ANESTESI', subtitle: '', icon: 'mdi-sleep', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 3, name: 'BEDAH', subtitle: '', icon: 'mdi-medical-bag', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 4, name: 'GERIATRI', subtitle: '', icon: 'mdi-account-supervisor', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 5, name: 'GIGI DAN MULUT', subtitle: '', icon: 'mdi-tooth', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 6, name: 'GIZI', subtitle: '', icon: 'mdi-food-apple', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 7, name: 'HEMATO ONKOLOGI MEDIS', subtitle: '', icon: 'mdi-water', shift: 'TUTUP', schedule: '', available: false, },
{ id: 8, name: 'IPD (PENYAKIT DALAM)', subtitle: '', icon: 'mdi-hospital', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 9, name: 'JANTUNG (CARDIOLOGI)', subtitle: '', icon: 'mdi-heart-pulse', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 10, name: 'JIWA', subtitle: '', icon: 'mdi-brain', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 11, name: 'KANDUNGAN', subtitle: '', icon: 'mdi-human-pregnant', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 12, name: 'KEMOTERAPI', subtitle: '', icon: 'mdi-needle', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 13, name: 'KOMPLEMENTER (NYERI)', subtitle: '', icon: 'mdi-leaf', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 14, name: 'KULIT KELAMIN', subtitle: '', icon: 'mdi-hand-back-right', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 15, name: 'MATA', subtitle: '', icon: 'mdi-eye', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 16, name: 'MCU', subtitle: '', icon: 'mdi-clipboard-check', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 17, name: 'ONKOLOGI', subtitle: '', icon: 'mdi-ribbon', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 18, name: 'PARU', subtitle: '', icon: 'mdi-lungs', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 19, name: 'R. TINDAKAN (EMG, ECG, DLL)', subtitle: '', icon: 'mdi-monitor-heart-rate', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 20, name: 'RADIOTERAPI', subtitle: '', icon: 'mdi-radioactive', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 21, name: 'REHAB MEDIK', subtitle: '', icon: 'mdi-human-cane', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, },
{ id: 22, name: 'SARAF (NEUROLOGI)', subtitle: '', icon: 'mdi-head-cog', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: false, },
{ id: 23, name: 'THT', subtitle: '', icon: 'mdi-ear-hearing', shift: 'SHIFT 1', schedule: 'Mulai Pukul 07:00', available: true, }
])
// Computed properties
// Computed properties (Tidak Berubah)
const filteredClinics = computed(() => {
let filtered = clinics.value
@@ -483,7 +400,7 @@ const filteredClinics = computed(() => {
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(clinic =>
filtered = filtered.filter(clinic =>
clinic.name.toLowerCase().includes(query) ||
clinic.subtitle.toLowerCase().includes(query)
)
@@ -501,17 +418,76 @@ const showSnackbar = (text, color = 'success') => {
snackbar.value = true
}
// 1. Memilih Klinik
const selectClinic = (clinic) => {
if (clinic.available) {
selectedClinic.value = clinic
showDialog.value = true
showVisitTypeDialog.value = true // Tampilkan dialog Pilih Kunjungan
}
}
const proceedToRegistration = () => {
showSnackbar(`Mengarahkan ke pendaftaran ${selectedClinic.value.name}...`, 'success')
console.log('Proceeding to registration for:', selectedClinic.value.name)
showDialog.value = false
// 2. Memilih Jenis Kunjungan (Sekarang, Pesan, Fast Track)
const selectVisitType = (type) => {
selectedVisitType.value = type
showVisitTypeDialog.value = false // Tutup dialog jenis kunjungan
if (type === 'SEKARANG') {
showPaymentTypeDialog.value = true // Lanjut ke dialog Jenis Pembayaran
} else if (type === 'PESAN') {
// Reset form booking sebelum ditampilkan
bookingForm.value = {
date: new Date().toISOString().substring(0, 10),
shift: 'Shift 1',
payment: null,
}
showBookingFormDialog.value = true // Tampilkan form Pesan
} else if (type === 'FAST TRACK') {
// Reset form fast track sebelum ditampilkan
fastTrackForm.value = {
guarantor: '',
noRek: '',
patientName: '',
reason: '',
payment: null,
}
showFastTrackFormDialog.value = true // Tampilkan form Fast Track
}
}
// 3. Memilih Jenis Pembayaran (untuk SEKARANG)
const selectPaymentType = (paymentType) => {
showPaymentTypeDialog.value = false
const message = `Pendaftaran **${selectedClinic.value.name}** untuk kunjungan **SEKARANG** dengan pembayaran **${paymentType}** berhasil diproses.`
showSnackbar(message, 'success')
console.log('Proceeding to registration for:', selectedClinic.value.name, 'Type: SEKARANG', 'Payment:', paymentType)
selectedClinic.value = null; // Reset
}
// 4. Submit Form Pesan
const submitBooking = () => {
if (!bookingForm.value.date || !bookingForm.value.shift || !bookingForm.value.payment) {
showSnackbar('Mohon lengkapi semua field Pemesanan.', 'error')
return
}
showBookingFormDialog.value = false
const message = `Pemesanan antrian **${selectedClinic.value.name}** pada ${bookingForm.value.date} (**${bookingForm.value.payment}**) berhasil disimpan.`
showSnackbar(message, 'success')
console.log('Submitted booking for:', selectedClinic.value.name, bookingForm.value)
selectedClinic.value = null; // Reset
}
// 5. Submit Form Fast Track
const submitFastTrack = () => {
if (!fastTrackForm.value.guarantor || !fastTrackForm.value.patientName || !fastTrackForm.value.reason || !fastTrackForm.value.payment) {
showSnackbar('Mohon lengkapi semua field Fast Track yang wajib diisi.', 'error')
return
}
showFastTrackFormDialog.value = false
const message = `Permintaan Fast Track **${selectedClinic.value.name}** atas nama ${fastTrackForm.value.patientName} (**${fastTrackForm.value.payment}**) telah diajukan.`
showSnackbar(message, 'success')
console.log('Submitted Fast Track for:', selectedClinic.value.name, fastTrackForm.value)
selectedClinic.value = null; // Reset
}
const refreshData = () => {
@@ -529,6 +505,7 @@ onMounted(() => {
</script>
<style scoped>
/* Style yang sudah ada (Tidak Berubah) */
.anjungan-container {
background: #f5f7fa;
min-height: 100vh;
@@ -595,7 +572,7 @@ onMounted(() => {
.clinic-card {
cursor: pointer;
border-radius: 16px !important;
height: 280px;
height: 240px;
background: white;
}
@@ -629,7 +606,7 @@ onMounted(() => {
margin-top: 8px;
}
/* Responsive Design */
/* Responsive Design (Tidak Berubah) */
@media (max-width: 1024px) {
.header-content {
flex-direction: column;
-1106
View File
File diff suppressed because it is too large Load diff
-526
View File
@@ -1,526 +0,0 @@
<template>
<!-- Main Content -->
<v-main class="bg-grey-lighten-3">
<v-container fluid class="pa-6 main-content-padding">
<!-- Header Banner & Stats -->
<v-card class="d-flex justify-space-between align-center pa-5 rounded-xl elevation-4 mb-6 header-banner">
<div class="d-flex align-center">
<v-icon size="40" class="mr-3 text-white">mdi-hospital-box-outline</v-icon>
<span class="text-h4 font-weight-bold text-white">Loket Admin </span>
</div>
<div class="d-flex align-center text-white text-end flex-wrap justify-end">
<span class="mr-4">Loket 24</span>
<span class="mr-4">{{ currentDateLongFormatted }}</span>
<span>{{ currentDateShortFormatted }} - Pelayanan</span>
</div>
</v-card>
<!-- Status Cards Section -->
<v-row class="mb-6">
<!-- Panggil 1 Antrian Card -->
<v-col cols="12" sm="6" md="3">
<v-card
class="pa-4 rounded-xl elevation-2 text-center"
color="#4CAF50"
@click="handleStatusCardClick(1)"
>
<v-card-text class="text-white">
<div class="text-h4 font-weight-bold">1</div>
<div class="text-subtitle-1 mt-1">Panggil</div>
</v-card-text>
</v-card>
</v-col>
<!-- Panggil 5 Antrian Card -->
<v-col cols="12" sm="6" md="3">
<v-card
class="pa-4 rounded-xl elevation-2 text-center"
color="#4CAF50"
@click="handleStatusCardClick(5)"
>
<v-card-text class="text-white">
<div class="text-h4 font-weight-bold">5</div>
<div class="text-subtitle-1 mt-1">Panggil</div>
</v-card-text>
</v-card>
</v-col>
<!-- Panggil 10 Antrian Card -->
<v-col cols="12" sm="6" md="3">
<v-card
class="pa-4 rounded-xl elevation-2 text-center"
color="#4CAF50"
@click="handleStatusCardClick(10)"
>
<v-card-text class="text-white">
<div class="text-h4 font-weight-bold">10</div>
<div class="text-subtitle-1 mt-1">Panggil</div>
</v-card-text>
</v-card>
</v-col>
<!-- Panggil 20 Antrian Card -->
<v-col cols="12" sm="6" md="3">
<v-card
class="pa-4 rounded-xl elevation-2 text-center"
color="#4CAF50"
@click="handleStatusCardClick(20)"
>
<v-card-text class="text-white">
<div class="text-h4 font-weight-bold">20</div>
<div class="text-subtitle-1 mt-1">Panggil</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Next Patient Card -->
<v-card class="next-patient-card d-flex align-center justify-center pa-8 text-center rounded-xl elevation-6 mb-6">
<div class="text-center">
<div class="text-h4 text-white">NEXT PATIENT</div>
<div class="text-h2 font-weight-bold text-white mt-2">UM1001</div>
<v-btn
size="large"
color="#00A896"
class="mt-4 text-white"
@click="handleNextPatientClick"
>
<v-icon start>mdi-arrow-right-circle</v-icon>
Panggil Pasien Selanjutnya
</v-btn>
</div>
</v-card>
<!-- Main Data Table -->
<v-card class="mb-6 pa-6 rounded-xl elevation-2">
<v-card-title class="d-flex justify-space-between align-center text-h5 font-weight-bold pa-0 mb-4">
Data Pasien
<div class="d-flex align-center">
<span class="mr-2 text-caption">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="solo"
flat
hide-details
class="mx-2 select-items"
></v-select>
<span class="mr-2 text-caption">entries</span>
<v-text-field
v-model="search"
density="compact"
variant="solo"
flat
hide-details
append-inner-icon="mdi-magnify"
label="Search"
style="max-width: 200px"
></v-text-field>
</div>
</v-card-title>
<v-data-table
:headers="mainHeaders"
:items="mainPatients"
:search="search"
:items-per-page="itemsPerPage"
:row-class="getRowClass"
class="custom-table"
>
<!-- Custom template for the 'aksi' column -->
<template v-slot:item.aksi="{ item }">
<div class="d-flex ga-1">
<!-- Show different buttons based on the item's status -->
<template v-if="item.status === 'dipanggil'">
<v-btn size="small" color="primary" variant="flat" class="rounded-lg" @click="handleProsesClick(item)">
<v-icon start>mdi-cogs</v-icon>Proses
</v-btn>
<v-btn size="small" color="success" variant="flat" class="rounded-lg">
<v-icon start>mdi-check-circle-outline</v-icon>Selesai
</v-btn>
</template>
<template v-else-if="item.status === 'dalam_proses'">
<v-btn size="small" color="success" variant="flat" class="rounded-lg">
<v-icon start>mdi-check-circle-outline</v-icon>Selesai
</v-btn>
<v-btn size="small" color="warning" variant="flat" class="rounded-lg">
<v-icon start>mdi-pause-circle-outline</v-icon>Tunda
</v-btn>
<v-btn size="small" color="error" variant="flat" class="rounded-lg">
<v-icon start>mdi-close-circle-outline</v-icon>Batal
</v-btn>
</template>
<template v-else>
<v-btn size="small" color="primary" variant="flat" class="rounded-lg">
<v-icon start>mdi-cogs</v-icon>Proses
</v-btn>
<v-btn size="small" color="success" variant="flat" class="rounded-lg">
<v-icon start>mdi-check-circle-outline</v-icon>Selesai
</v-btn>
</template>
</div>
</template>
<!-- Custom template for the 'panggil' column -->
<template v-slot:item.panggil="{ item }">
<v-btn
size="small"
:color="item.status === 'dalam_proses' ? 'grey' : 'info'"
variant="flat"
class="rounded-lg"
@click="handlePanggilClick(item)"
:disabled="item.status === 'dalam_proses'"
>
<v-icon start>mdi-phone</v-icon>Panggil
</v-btn>
</template>
<!-- Custom template for the 'noAntrian' column -->
<template v-slot:item.noAntrian="{ item }">
<span :class="{'online-antrian': item.status === 'dipanggil'}">{{ item.noAntrian }}</span>
</template>
</v-data-table>
</v-card>
<!-- Late Patients Table -->
<v-card class="mb-6 pa-6 rounded-xl elevation-2">
<v-card-title class="d-flex justify-space-between align-center text-h5 font-weight-bold pa-0 mb-4">
Info Pasien Lapor Terlambat
<div class="d-flex align-center">
<span class="mr-2 text-caption text-orange">KETERANGAN: PASIEN MASUK PADA TANGGAL</span>
<span class="mr-2 text-caption">Show</span>
<v-select
v-model="lateItemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="solo"
flat
hide-details
class="mx-2 select-items"
></v-select>
<span class="mr-2 text-caption">entries</span>
<v-text-field
v-model="lateSearch"
density="compact"
variant="solo"
flat
hide-details
append-inner-icon="mdi-magnify"
label="Search"
style="max-width: 200px"
></v-text-field>
</div>
</v-card-title>
<v-data-table
:headers="lateHeaders"
:items="latePatients"
:search="lateSearch"
:items-per-page="lateItemsPerPage"
class="custom-table"
>
<template v-slot:no-data>
<div class="text-center pa-4">Tidak ada data yang tersedia</div>
</template>
</v-data-table>
</v-card>
<!-- Clinic Entry Patients Table -->
<v-card class="mb-6 pa-6 rounded-xl elevation-2">
<v-card-title class="d-flex justify-space-between align-center text-h5 font-weight-bold pa-0 mb-4">
Info Pasien Masuk Klinik
</v-card-title>
<v-data-table
:headers="clinicHeaders"
:items="clinicPatients"
:search="clinicSearch"
:items-per-page="clinicItemsPerPage"
class="custom-table"
>
<template v-slot:no-data>
<div class="text-center pa-4">Tidak ada data yang tersedia</div>
</template>
</v-data-table>
</v-card>
<!-- Info Klinik Table -->
<v-card class="pa-6 rounded-xl elevation-2">
<v-card-title class="text-h5 font-weight-bold pa-0 mb-4">
Info Klinik
</v-card-title>
<v-data-table
:headers="infoKlinikHeaders"
:items="infoKlinikData"
class="custom-table"
hide-default-footer
disable-pagination
>
<template v-slot:bottom>
<v-card-text class="d-flex justify-end text-right">
<span class="mr-4 font-weight-bold text-h6">Total:</span>
<span class="mr-12 font-weight-bold text-h6 text-primary">{{ totalDapatDipanggil }}</span>
<span class="font-weight-bold text-h6 text-primary">{{ totalShiftBelumBuka }}</span>
</v-card-text>
</template>
</v-data-table>
</v-card>
</v-container>
</v-main>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
definePageMeta({
middleware:['auth']
})
// Reactive data
const search = ref("");
const lateSearch = ref("");
const clinicSearch = ref("");
const itemsPerPage = ref(10);
const lateItemsPerPage = ref(10);
const clinicItemsPerPage = ref(10);
const currentDateLongFormatted = ref("");
const currentDateShortFormatted = ref("");
// Table headers
const mainHeaders = ref([
{ title: "No", value: "no", sortable: false },
{ title: "Jam Panggil", value: "jamPanggil" },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Fast Track", value: "fastTrack" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Panggil", align: 'center', value: "panggil", sortable: false },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const lateHeaders = ref([
{ title: "No", value: "no", sortable: false },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const clinicHeaders = ref([
{ title: "#", value: "no", sortable: false },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "No RM", value: "noRM" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Fast Track", value: "fastTrack" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const infoKlinikHeaders = ref([
{ title: "#", value: "no" },
{ title: "Klinik", value: "klinik" },
{ title: "Jumlah Shift", value: "jumlahShift" },
{ title: "Quota Per Shift", value: "quotaPerShift" },
{ title: "Status", value: "status" },
{ title: "Dapat Di Panggil", value: "dapatDiPanggil" },
{ title: "Shift Belum Buka", value: "shiftBelumBuka" },
]);
// Sample data with new 'originalAntrian' and 'status' properties
const mainPatients = ref([
{ no: 1, jamPanggil: "11:46", barcode: "250826100362", noAntrian: "UM1002 | Online - 250826100362", originalAntrian: "UM1002", shift: "Shift 1", klinik: "IPD", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "dipanggil" },
{ no: 2, jamPanggil: "06:47", barcode: "250826100140", noAntrian: "UM1003", originalAntrian: "UM1003", shift: "Shift 1", klinik: "IPD", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "" },
{ no: 3, jamPanggil: "06:47", barcode: "250826100143", noAntrian: "UM1004", originalAntrian: "UM1004", shift: "Shift 1", klinik: "IPD", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "" },
{ no: 4, jamPanggil: "06:47", barcode: "250826100500", noAntrian: "UM1005", originalAntrian: "UM1005", shift: "Shift 1", klinik: "MATA", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "" },
{ no: 5, jamPanggil: "06:47", barcode: "250826100525", noAntrian: "UM1006", originalAntrian: "UM1006", shift: "Shift 1", klinik: "ONKOLOGI", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "" },
{ no: 6, jamPanggil: "06:47", barcode: "250826100536", noAntrian: "UM1007", originalAntrian: "UM1007", shift: "Shift 1", klinik: "THT", fastTrack: "", pembayaran: "UMUM", panggil: "Panggil", status: "" },
]);
// Tambahkan lebih banyak data pasien untuk demonstrasi "Panggil 20"
for (let i = 7; i <= 25; i++) {
mainPatients.value.push({
no: i,
jamPanggil: "07:00",
barcode: `250826100${100 + i}`,
noAntrian: `UM100${i}`,
originalAntrian: `UM100${i}`,
shift: "Shift 1",
klinik: "UMUM",
fastTrack: "",
pembayaran: "UMUM",
panggil: "Panggil",
status: "",
});
}
const latePatients = ref([]);
const clinicPatients = ref([]);
const infoKlinikData = ref([
{ no: 1, klinik: "ANESTESI", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 2, klinik: "GERIATRI", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 3, klinik: "GIGI DAN MULUT", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 4, klinik: "HOM", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 5, klinik: "IPD", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 6, klinik: "JANTUNG", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 7, klinik: "KANDUNGAN", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 8, klinik: "KOMPLEMENTER", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBukan: "-" },
{ no: 9, klinik: "KUL.KEL", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 10, klinik: "MATA", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 11, klinik: "ONKOLOGI", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 12, klinik: "PARU", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 13, klinik: "SARAF", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
{ no: 14, klinik: "THT", jumlahShift: "1 Shift", quotaPerShift: 1000, status: "Buka - Shift 1", dapatDiPanggil: "-", shiftBelumBuka: "-" },
]);
// Computed properties for totals
const totalDapatDipanggil = computed(() => {
return infoKlinikData.value.reduce((total, item) => {
return total + (item.dapatDiPanggil === "-" ? 0 : parseInt(item.dapatDiPanggil));
}, 0);
});
const totalShiftBelumBuka = computed(() => {
return infoKlinikData.value.reduce((total, item) => {
return total + (item.shiftBelumBuka === "-" ? 0 : parseInt(item.shiftBelumBuka));
}, 0);
});
// Methods
const getRowClass = (item) => {
if (item.status === 'dipanggil') {
return 'called-row';
}
return '';
};
const handleStatusCardClick = (count) => {
console.log(`Memanggil ${count} antrean pasien.`);
const updatedPatients = mainPatients.value.map((patient, index) => {
const isCalled = index < count;
return {
...patient,
status: isCalled ? 'dipanggil' : '',
noAntrian: isCalled ? `${patient.originalAntrian} | Online - ${patient.barcode}` : patient.originalAntrian
};
});
mainPatients.value = updatedPatients;
};
const handleNextPatientClick = () => {
console.log("Tombol Panggil Pasien Selanjutnya diklik! (Mengambil pasien pertama dari antrean)");
const updatedPatients = mainPatients.value.map(patient => {
return { ...patient, status: '', noAntrian: patient.originalAntrian };
});
mainPatients.value = updatedPatients;
};
const handlePanggilClick = (item) => {
console.log(`Tombol Panggil untuk pasien: ${item.noAntrian} diklik!`);
// Membuat salinan baru dari seluruh array untuk memicu reaktivitas
const updatedPatients = mainPatients.value.map(p => {
// Jika pasien cocok, buat salinan baru dengan status 'dipanggil' dan tambahkan "Online"
if (p.no === item.no) {
return {
...p,
status: 'dipanggil',
noAntrian: `${p.originalAntrian} | Online - ${p.barcode}`
};
}
// Jika tidak, kembalikan objek pasien aslinya
return p;
});
// Ganti seluruh array data dengan salinan yang baru.
mainPatients.value = updatedPatients;
};
const handleProsesClick = (item) => {
console.log(`Tombol Proses untuk pasien: ${item.noAntrian} diklik!`);
const updatedPatients = mainPatients.value.map(p => {
if (p.no === item.no) {
return {
...p,
status: 'dalam_proses'
};
}
return p;
});
mainPatients.value = updatedPatients;
};
// Mengatur tanggal saat komponen dimuat
onMounted(() => {
const today = new Date();
const optionsLong = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
currentDateLongFormatted.value = today.toLocaleDateString('id-ID', optionsLong);
const optionsShort = { year: 'numeric', month: 'long', day: 'numeric' };
currentDateShortFormatted.value = today.toLocaleDateString('id-ID', optionsShort);
});
</script>
<style scoped>
/* Scoped styles for a cleaner look */
.main-content-padding {
padding-left: 24px !important;
padding-right: 24px !important;
}
/* Header Banner */
.header-banner {
background: linear-gradient(90deg, #1565C0, #1976D2);
color: white;
min-height: 120px;
}
/* Next Patient Card */
.next-patient-card {
background: linear-gradient(45deg, #00A896, #00796B);
color: white;
}
/* Status Cards */
.v-card.text-center {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
cursor: pointer;
}
.v-card.text-center:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
/* Table styling */
.custom-table :deep(thead th) {
background-color: #E8EAF6; /* Light gray-blue header */
font-weight: bold;
}
.custom-table :deep(tbody tr:nth-of-type(odd)) {
background-color: #F5F5F5; /* Light gray for odd rows */
}
/* Highlighted row for "dipanggil" status */
.custom-table :deep(tbody tr.called-row) {
background-color: #998479 !important; /* Light green background */
}
/* Field and Select styling */
.select-items .v-field--variant-solo,
.v-text-field .v-field--variant-solo {
background-color: #ECEFF1;
}
.text-blue {
color: #1976D2 !important;
}
.text-primary {
color: #1976D2 !important;
}
.online-antrian {
font-weight: bold;
color: #1976D2;
}
</style>
+354
View File
@@ -0,0 +1,354 @@
// stores/queueStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useQueueStore = defineStore('queue', () => {
// State
const allPatients = ref([
{
no: 1,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting", // waiting, di-loket, terlambat, pending, processed
processStage: "loket", // loket, klinik, penunjang
createdAt: new Date().toISOString(),
},
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 3,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1003 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 4,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1004 | Online - 250811100210",
shift: "Shift 1",
klinik: "THT",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
]);
const quotaUsed = ref(5);
const currentProcessingPatient = ref({
loket: null,
klinik: null,
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" },
]);
const penunjangs = ref([
{ id: 1, name: "LABORATORIUM" },
{ id: 2, name: "RADIOLOGI" },
{ id: 3, name: "USG" },
{ id: 4, name: "CT SCAN" },
{ id: 5, name: "MRI" },
{ id: 6, name: "EKG" },
{ id: 7, name: "ENDOSKOPI" },
{ id: 8, name: "FISIOTERAPI" },
{ id: 9, name: "FARMASI" },
]);
// Computed - Filter berdasarkan process stage dan status
const getPatientsByStage = (stage) => {
return computed(() => {
const patients = allPatients.value.filter(p => p.processStage === stage);
return {
all: patients,
waiting: patients.filter(p => p.status === 'waiting'),
diLoket: patients.filter(p => p.status === 'di-loket'),
terlambat: patients.filter(p => p.status === 'terlambat'),
pending: patients.filter(p => p.status === 'pending'),
};
});
};
// Total pasien per stage
const getTotalPasienByStage = (stage) => {
return computed(() =>
allPatients.value.filter(p => p.processStage === stage).length
);
};
const totalPasien = computed(() => allPatients.value.length);
// Actions
const callNext = (adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const nextPatient = allPatients.value.find(p =>
p.status === 'waiting' && p.processStage === targetStage
);
if (!nextPatient) {
return { success: false, message: "Tidak ada pasien selanjutnya" };
}
if (quotaUsed.value >= 150) {
return { success: false, message: "Quota sudah penuh" };
}
nextPatient.status = "di-loket";
currentProcessingPatient.value[adminType] = nextPatient;
quotaUsed.value++;
return {
success: true,
message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]}`,
};
};
const callMultiplePatients = (count, adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const waitingList = allPatients.value.filter(p =>
p.status === 'waiting' && p.processStage === targetStage
);
const patientsToCall = waitingList.slice(0, count);
if (patientsToCall.length === 0) {
return { success: false, message: "Tidak ada pasien yang menunggu" };
}
if (quotaUsed.value + patientsToCall.length > 150) {
return { success: false, message: "Quota tidak mencukupi" };
}
patientsToCall.forEach((patient) => {
patient.status = "di-loket";
});
quotaUsed.value += patientsToCall.length;
return {
success: true,
message: `Memanggil ${patientsToCall.length} pasien ke loket`,
};
};
const processPatient = (patient, action, adminType = 'loket') => {
const patientCode = patient.noAntrian.split(" |")[0];
let message = "";
switch (action) {
case "check-in":
// Jika check-in di loket, pindahkan ke klinik dengan status di-loket
if (adminType === 'loket') {
patient.status = "di-loket"; // Langsung masuk tabel Di Loket Klinik
patient.processStage = "klinik";
message = `Pasien ${patientCode} berhasil check in dan masuk ke Tabel Loket Klinik`;
}
// Jika check-in di klinik, selesai
else if (adminType === 'klinik') {
patient.status = "processed";
message = `Pasien ${patientCode} berhasil check in di Klinik`;
}
// Jika check-in di penunjang, selesai
else if (adminType === 'penunjang') {
patient.status = "processed";
message = `Pasien ${patientCode} berhasil check in di Penunjang`;
}
// Clear current processing di admin yang melakukan check-in
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
break;
case "terlambat":
patient.status = "terlambat";
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
message = `Pasien ${patientCode} ditandai terlambat`;
break;
case "pending":
patient.status = "pending";
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
message = `Pasien ${patientCode} di-pending`;
break;
case "aktifkan":
if (patient.status === "terlambat" || patient.status === "pending") {
patient.status = "di-loket";
message = `Pasien ${patientCode} diaktifkan kembali`;
}
break;
case "proses":
currentProcessingPatient.value[adminType] = patient;
message = `Memproses pasien ${patientCode}`;
break;
}
return { success: true, message };
};
const createAntreanKlinik = (klinik, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = `250811${String(timestamp.getTime()).slice(-6)}`;
const newPatient = {
no: newNo,
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
timestamp.getMinutes()
).padStart(2, "0")}`,
barcode: barcode,
noAntrian: `KL${String(newNo).padStart(4, "0")} | Klinik - ${barcode}`,
shift: "Shift 1",
klinik: klinik.name,
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "di-loket", // Langsung masuk tabel Di Loket Klinik
processStage: "klinik",
createdAt: timestamp.toISOString(),
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Antrean klinik ${klinik.name} berhasil dibuat dan masuk ke Tabel Loket Klinik`,
patient: newPatient,
};
};
const createAntreanPenunjang = (penunjang, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const newPatient = {
no: newNo,
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
timestamp.getMinutes()
).padStart(2, "0")}`,
barcode: barcode,
noAntrian: `PN${String(newNo).padStart(4, "0")} | Penunjang - ${barcode}`,
shift: "Shift 1",
klinik: penunjang.name,
fastTrack: "UMUM",
pembayaran: patient ? patient.pembayaran : "UMUM",
status: "di-loket", // Langsung masuk tabel Di Loket Penunjang
processStage: "penunjang",
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Antrean penunjang ${penunjang.name} berhasil dibuat dan masuk ke Tabel Loket Penunjang`,
patient: newPatient,
};
};
const changeKlinik = (patient, newKlinik, adminType = 'loket') => {
const patientIndex = allPatients.value.findIndex((p) => p.no === patient.no);
if (patientIndex !== -1) {
allPatients.value[patientIndex].klinik = newKlinik.name;
// Update current processing if it's the same patient
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType].klinik = newKlinik.name;
}
return {
success: true,
message: `Klinik berhasil diubah ke ${newKlinik.name}`,
};
}
return { success: false, message: "Pasien tidak ditemukan" };
};
const getCurrentProcessing = (adminType) => {
return computed(() => currentProcessingPatient.value[adminType]);
};
const setCurrentProcessing = (patient, adminType) => {
currentProcessingPatient.value[adminType] = patient;
};
return {
// State
allPatients,
quotaUsed,
currentProcessingPatient,
kliniks,
penunjangs,
// Computed
totalPasien,
// Actions
callNext,
callMultiplePatients,
processPatient,
createAntreanKlinik,
createAntreanPenunjang,
changeKlinik,
getPatientsByStage,
getTotalPasienByStage,
getCurrentProcessing,
setCurrentProcessing,
};
});