klinik ruang dan update admin loket

This commit is contained in:
bagus-arie05
2026-01-09 11:08:58 +07:00
parent 1b7a142162
commit 9ea8300973
4 changed files with 333 additions and 95 deletions
+310 -42
View File
@@ -10,7 +10,8 @@
/>
<!-- Rooms Grid -->
<div class="rooms-grid">
<div class="rooms-grid-wrapper">
<div class="rooms-grid">
<v-card
v-for="ruang in ruangList"
:key="`${klinikData?.kodeKlinik}-${ruang.nomorRuang}`"
@@ -183,6 +184,7 @@
</div>
</v-card-text>
</v-card>
</div>
</div>
<!-- Dialog Pindah Ruang -->
@@ -221,74 +223,118 @@
</v-dialog>
<!-- Dialog Patient Detail -->
<v-dialog v-model="showDetailDialog" max-width="500px">
<v-card>
<v-card-title class="dialog-header">
<span>Detail Pasien</span>
<v-btn icon variant="text" size="small" @click="showDetailDialog = false">
<v-dialog v-model="showDetailDialog" max-width="600px" persistent>
<v-card class="detail-dialog-card">
<v-card-title class="detail-dialog-header">
<span class="detail-dialog-title">Detail Pasien</span>
<v-btn icon variant="text" size="small" class="detail-close-btn" @click="showDetailDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider />
<v-card-text class="pa-4" v-if="selectedPatientDetail">
<v-row dense>
<v-card-text class="detail-dialog-content" v-if="selectedPatientDetail">
<v-row dense class="detail-row">
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Nomor Antrian</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.noAntrian?.split(" |")[0] }}</span>
<span class="detail-label">Nomor Antrian</span>
<span class="detail-value">{{ selectedPatientDetail.noAntrian?.split(" |")[0] || '-' }}</span>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Barcode</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.barcode }}</span>
<span class="detail-label">Barcode</span>
<span class="detail-value">{{ selectedPatientDetail.barcode || '-' }}</span>
</div>
</v-col>
</v-row>
<v-row dense class="detail-row">
<v-col cols="6">
<div class="detail-item">
<span class="detail-label">Klinik</span>
<span class="detail-value">{{ selectedPatientDetail.klinik || '-' }}</span>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Klinik</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.klinik }}</span>
<span class="detail-label">Ruang</span>
<span class="detail-value">{{ selectedPatientDetail.ruang || '-' }}</span>
</div>
</v-col>
</v-row>
<v-row dense class="detail-row">
<v-col cols="6">
<div class="detail-item">
<span class="detail-label">Jam Panggil</span>
<span class="detail-value">{{ selectedPatientDetail.jamPanggil || '-' }}</span>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Ruang</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.ruang || '-' }}</span>
<span class="detail-label">Pembayaran</span>
<span class="detail-value">{{ selectedPatientDetail.pembayaran || '-' }}</span>
</div>
</v-col>
</v-row>
<v-row dense class="detail-row">
<v-col cols="6">
<div class="detail-item">
<span class="detail-label">Layanan</span>
<div class="detail-chips-group">
<v-chip
size="small"
:color="selectedPatientDetail.calledPemeriksaanAwal ? 'primary' : 'grey'"
:variant="selectedPatientDetail.calledPemeriksaanAwal ? 'flat' : 'outlined'"
class="detail-chip-clickable"
:class="{ 'chip-active': selectedPatientDetail.calledPemeriksaanAwal }"
@click="toggleLayananStatus('Pemeriksaan Awal')"
>
Pemeriksaan Awal
</v-chip>
<v-chip
size="small"
:color="selectedPatientDetail.calledTindakan ? 'secondary' : 'grey'"
:variant="selectedPatientDetail.calledTindakan ? 'flat' : 'outlined'"
class="detail-chip-clickable"
:class="{ 'chip-active': selectedPatientDetail.calledTindakan }"
@click="toggleLayananStatus('Tindakan')"
>
Tindakan
</v-chip>
</div>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Jam Panggil</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.jamPanggil }}</span>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Pembayaran</span>
<span class="body-2 text-semibold">{{ selectedPatientDetail.pembayaran }}</span>
</div>
</v-col>
<v-col cols="6" v-if="selectedPatientDetail.tipeLayanan">
<div class="detail-item">
<span class="caption-2 text-muted">Tipe Layanan</span>
<v-chip size="small" :color="selectedPatientDetail.tipeLayanan === 'Pemeriksaan Awal' ? 'primary' : 'secondary'">
{{ selectedPatientDetail.tipeLayanan }}
</v-chip>
</div>
</v-col>
<v-col cols="6">
<div class="detail-item">
<span class="caption-2 text-muted">Status</span>
<v-chip size="small" :color="getStatusColor(selectedPatientDetail.status)">
<span class="detail-label">Status</span>
<v-chip
size="small"
:color="getStatusColor(selectedPatientDetail.status)"
class="detail-chip"
>
{{ getStatusLabel(selectedPatientDetail.status) }}
</v-chip>
</div>
</v-col>
</v-row>
<v-row dense class="detail-row" v-if="selectedPatientDetail.referencePatient">
<v-col cols="12">
<div class="detail-item">
<span class="detail-label">Nomor Antrean Anjungan Sebelumnya</span>
<span class="detail-value reference-value">{{ selectedPatientDetail.referencePatient }}</span>
</div>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-card-actions class="detail-dialog-actions">
<v-spacer />
<v-btn variant="text" @click="showDetailDialog = false">Tutup</v-btn>
<v-btn
color="primary"
variant="flat"
class="text-white"
@click="showDetailDialog = false"
>
TUTUP
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -571,9 +617,11 @@ const handleCallPatientByTipe = (ruang, tipeLayanan) => {
}
// Update tracking panggilan berdasarkan tipeLayanan
// Update tipeLayanan pasien agar muncul di kolom yang sesuai di Anjungan
const updateData = {
...queueStore.allPatients[patientIndex],
status: 'di-loket',
tipeLayanan: tipeLayanan, // Update tipeLayanan untuk display di Anjungan
lastCalledAt: new Date().toISOString(),
lastCalledTipeLayanan: tipeLayanan
};
@@ -598,10 +646,76 @@ const handleCallPatientByTipe = (ruang, tipeLayanan) => {
};
const showPatientDetail = (patient) => {
selectedPatientDetail.value = patient;
// Get latest patient data from store
const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
if (patientIndex !== -1) {
selectedPatientDetail.value = { ...queueStore.allPatients[patientIndex] };
} else {
selectedPatientDetail.value = { ...patient };
}
showDetailDialog.value = true;
};
// Toggle layanan status (membatalkan panggilan jika sudah dipanggil)
const toggleLayananStatus = (tipeLayanan) => {
if (!selectedPatientDetail.value || !selectedPatientDetail.value.no) return;
const patientIndex = queueStore.allPatients.findIndex(p => p.no === selectedPatientDetail.value.no);
if (patientIndex === -1) {
showSnackbar('Pasien tidak ditemukan', 'error');
return;
}
const patient = queueStore.allPatients[patientIndex];
const updateData = { ...patient };
if (tipeLayanan === 'Pemeriksaan Awal') {
// Toggle status: jika sudah dipanggil, batalkan; jika belum, tidak bisa diaktifkan dari sini
if (patient.calledPemeriksaanAwal) {
updateData.calledPemeriksaanAwal = false;
// Jika tipeLayanan saat ini adalah Pemeriksaan Awal, reset tipeLayanan
if (patient.tipeLayanan === 'Pemeriksaan Awal') {
updateData.tipeLayanan = null;
}
showSnackbar('Status panggilan Pemeriksaan Awal dibatalkan', 'info');
} else {
showSnackbar('Pemeriksaan Awal belum dipanggil', 'warning');
return;
}
} else if (tipeLayanan === 'Tindakan') {
// Toggle status: jika sudah dipanggil, batalkan; jika belum, tidak bisa diaktifkan dari sini
if (patient.calledTindakan) {
updateData.calledTindakan = false;
// Jika tipeLayanan saat ini adalah Tindakan, reset tipeLayanan
if (patient.tipeLayanan === 'Tindakan') {
updateData.tipeLayanan = null;
}
showSnackbar('Status panggilan Tindakan dibatalkan', 'info');
} else {
showSnackbar('Tindakan belum dipanggil', 'warning');
return;
}
}
// Update patient data
queueStore.allPatients[patientIndex] = updateData;
// Update selectedPatientDetail untuk refresh dialog
selectedPatientDetail.value = { ...updateData };
// Update current processing jika pasien sedang diproses
const ruangList = masterStore.ruangData || [];
for (const klinikRuang of ruangList) {
if (klinikRuang.kodeKlinik === patient.kodeKlinik) {
const key = `klinik-ruang-${patient.kodeKlinik}-${patient.nomorRuang}`;
if (queueStore.currentProcessingPatient[key]?.no === patient.no) {
queueStore.currentProcessingPatient[key] = updateData;
}
break;
}
}
};
const getStatusColor = (status) => {
const colors = {
'di-loket': 'success',
@@ -767,9 +881,13 @@ onMounted(() => {
line-height: 1.4;
}
.rooms-grid-wrapper {
margin-top: 24px;
}
.rooms-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@@ -791,6 +909,7 @@ onMounted(() => {
align-items: center;
font-weight: 700;
font-size: 16px;
gap: 8px;
}
.current-patient-section {
@@ -888,6 +1007,149 @@ onMounted(() => {
padding: 16px 20px;
}
/* Detail Dialog Styles */
.detail-dialog-card {
border-radius: 12px;
overflow: hidden;
}
.detail-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-neutral-100);
color: var(--color-neutral-900);
padding: 20px 24px;
border-bottom: 1px solid var(--color-neutral-300);
}
.detail-dialog-title {
font-size: 20px;
font-weight: 700;
color: var(--color-neutral-900);
}
.detail-close-btn {
color: var(--color-neutral-600) !important;
}
.detail-close-btn:hover {
background: var(--color-neutral-200) !important;
}
.detail-dialog-content {
padding: 24px !important;
background: var(--color-neutral-50);
}
.detail-row {
margin-bottom: 16px;
}
.detail-row:last-child {
margin-bottom: 0;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-label {
font-size: 12px;
line-height: 16px;
color: var(--color-neutral-600);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 16px;
line-height: 24px;
font-weight: 700;
color: var(--color-neutral-900);
}
.detail-value.reference-value {
font-size: 14px;
line-height: 20px;
word-break: break-word;
}
.detail-chip {
font-weight: 600;
text-transform: none;
}
.detail-chips-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.detail-chip-clickable {
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
text-transform: none;
}
.detail-chip-clickable:hover {
transform: scale(1.05);
opacity: 0.9;
}
.detail-chip-clickable.chip-active {
cursor: pointer;
}
.detail-chip-clickable:not(.chip-active) {
cursor: not-allowed;
opacity: 0.6;
}
.detail-dialog-actions {
padding: 16px 24px;
background: var(--color-neutral-100);
border-top: 1px solid var(--color-neutral-300);
}
.detail-dialog-actions .v-btn {
min-width: 120px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-chips-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.detail-chip-clickable {
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
text-transform: none;
}
.detail-chip-clickable:hover {
transform: scale(1.05);
opacity: 0.9;
}
.detail-chip-clickable.chip-active {
cursor: pointer;
}
.detail-chip-clickable:not(.chip-active) {
cursor: not-allowed;
opacity: 0.6;
}
.body-2 {
font-size: 16px;
line-height: 24px;
@@ -979,6 +1241,12 @@ onMounted(() => {
font-weight: 600;
}
@media (max-width: 1400px) {
.rooms-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 960px) {
.rooms-grid {
grid-template-columns: 1fr;
+3 -19
View File
@@ -37,21 +37,8 @@
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-row dense>
<v-col cols="4">
<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="4">
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
@@ -64,7 +51,7 @@
</v-btn>
</v-col>
<v-col cols="4">
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
@@ -559,10 +546,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
}
.create-buttons :deep(.v-col) {
padding: 0 4px;
box-sizing: border-box;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.create-buttons :deep(.v-col:first-child) {
@@ -58,19 +58,7 @@
{{ ruang.pemeriksaanAwal.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<div v-if="ruang.pemeriksaanAwal.nextQueues.length > 0" class="next-section">
<div class="next-label">SELANJUTNYA</div>
<div class="next-numbers">
<div
v-for="queue in ruang.pemeriksaanAwal.nextQueues"
:key="queue.no"
class="next-item"
>
{{ queue.noAntrian.split(' |')[0] }}
</div>
</div>
</div>
<div v-if="!ruang.pemeriksaanAwal.currentQueue && ruang.pemeriksaanAwal.nextQueues.length === 0" class="empty-state-small">
<div v-if="!ruang.pemeriksaanAwal.currentQueue" class="empty-state-small">
<v-icon size="24" color="grey-lighten-3">mdi-clock-outline</v-icon>
<p class="empty-text-small">Tidak Ada Antrian</p>
</div>
@@ -88,19 +76,7 @@
{{ ruang.tindakan.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<div v-if="ruang.tindakan.nextQueues.length > 0" class="next-section">
<div class="next-label">SELANJUTNYA</div>
<div class="next-numbers">
<div
v-for="queue in ruang.tindakan.nextQueues"
:key="queue.no"
class="next-item"
>
{{ queue.noAntrian.split(' |')[0] }}
</div>
</div>
</div>
<div v-if="!ruang.tindakan.currentQueue && ruang.tindakan.nextQueues.length === 0" class="empty-state-small">
<div v-if="!ruang.tindakan.currentQueue" class="empty-state-small">
<v-icon size="24" color="grey-lighten-3">mdi-clock-outline</v-icon>
<p class="empty-text-small">Tidak Ada Antrian</p>
</div>
@@ -280,28 +256,39 @@ const displayedRuang = computed(() => {
return numA - numB
})
// Split by tipeLayanan
const pemeriksaanAwalQueues = allQueues.filter(q => q.tipeLayanan === 'Pemeriksaan Awal')
const tindakanQueues = allQueues.filter(q => q.tipeLayanan === 'Tindakan')
// Split by tipeLayanan berdasarkan tipeLayanan terakhir yang dipanggil
// Pasien yang dipanggil akan muncul di kolom sesuai tipeLayanan terakhir yang dipanggil
// Jika pasien dipanggil untuk Pemeriksaan Awal, muncul di kolom Pemeriksaan Awal
// Jika pasien dipanggil untuk Tindakan, muncul di kolom Tindakan (dan hilang dari Pemeriksaan Awal)
const pemeriksaanAwalQueues = allQueues.filter(q => {
// Gunakan tipeLayanan (yang diupdate saat dipanggil) untuk menentukan kolom
return q.tipeLayanan === 'Pemeriksaan Awal';
});
const tindakanQueues = allQueues.filter(q => {
// Gunakan tipeLayanan (yang diupdate saat dipanggil) untuk menentukan kolom
return q.tipeLayanan === 'Tindakan';
})
// Get current and next queues for each tipeLayanan
// Current queue is the one with status 'di-loket' and matching tipeLayanan
// Jika ada pasien baru yang dipanggil, akan menggantikan pasien sebelumnya di kolom tersebut
const getCurrentAndNext = (queues, tipeLayanan) => {
// Find current: pasien dengan status 'di-loket' dan tipeLayanan yang sesuai
// Sort by lastCalledAt untuk mendapatkan yang terakhir dipanggil
// Sort by lastCalledAt untuk mendapatkan yang terakhir dipanggil (yang baru dipanggil akan menggantikan yang lama)
const currentCandidates = queues.filter(q =>
q.status === 'di-loket' && q.tipeLayanan === tipeLayanan
).sort((a, b) => {
const timeA = a.lastCalledAt ? new Date(a.lastCalledAt) : new Date(a.createdAt || 0)
const timeB = b.lastCalledAt ? new Date(b.lastCalledAt) : new Date(b.createdAt || 0)
return timeB - timeA
return timeB - timeA // Yang terakhir dipanggil di atas
})
// Ambil yang terakhir dipanggil sebagai current (menggantikan yang sebelumnya)
const current = currentCandidates.length > 0 ? currentCandidates[0] : null
// Next queues: exclude current patient
// Next queues: exclude current patient dan pasien dengan status 'di-loket' (yang sudah dipanggil)
const next = queues.filter(q =>
q.no !== current?.no &&
(q.status === 'waiting' || q.status === 'di-loket')
q.status === 'waiting' // Hanya yang waiting (belum dipanggil)
).slice(0, 3) // Show max 3 next queues
return { current, next }
}
-1
View File
@@ -24,7 +24,6 @@ const defaultNavItems: NavItem[] = [
// badge: "3",
},
{ id: 4, name: "Admin Loket", icon: "mdi-account-supervisor-outline", path: "/AdminLoket" },
{ id: 5, name: "Admin Klinik", icon: "mdi-text-box-plus-outline", path: "/AdminKlinik" },
{ id: 6, name: "Admin Klinik Ruang", icon: "mdi-door-open", path: "/AdminKlinikRuang" },
{ id: 7, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
{ id: 8, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },