update no rm dan desain

This commit is contained in:
bagus-arie05
2026-01-22 09:32:43 +07:00
parent f3b50bd0a3
commit 2ccb378672
6 changed files with 506 additions and 571 deletions

No files matched your search

@@ -34,66 +34,55 @@
</div>
</div>
<div class="action-icons">
<v-tooltip :text="changeButtonText" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="secondary-600"
variant="flat"
size="small"
@click="$emit('change-klinik')"
>
<v-icon size="18">mdi-swap-horizontal</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip text="Terlambat" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="primary-600"
variant="flat"
size="small"
@click="$emit('action', 'terlambat')"
>
<v-icon size="18">mdi-clock-alert</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip text="Pending" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="danger-600"
variant="flat"
size="small"
@click="$emit('action', 'pending')"
>
<v-icon size="18">mdi-pause</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip text="Selesai" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="success-600"
variant="flat"
size="small"
@click="$emit('action', 'check-in')"
>
<v-icon size="18">mdi-check</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-btn
class="action-icon-btn text-white"
color="secondary-600"
variant="flat"
size="small"
@click="$emit('change-klinik')"
>
<v-icon size="16">mdi-swap-horizontal</v-icon>
<span class="btn-label">{{ changeButtonText }}</span>
</v-btn>
<v-btn
class="action-icon-btn text-white"
color="primary-600"
variant="flat"
size="small"
@click="$emit('action', 'terlambat')"
>
<v-icon size="16">mdi-clock-alert</v-icon>
<span class="btn-label">Terlambat</span>
</v-btn>
<v-btn
class="action-icon-btn text-white"
color="danger-600"
variant="flat"
size="small"
@click="$emit('action', 'pending')"
>
<v-icon size="16">mdi-pause</v-icon>
<span class="btn-label">Pending</span>
</v-btn>
<v-btn
class="action-icon-btn text-white"
color="success-600"
variant="flat"
size="small"
@click="$emit('action', 'check-in')"
>
<v-icon size="16">mdi-check</v-icon>
<span class="btn-label">Selesai</span>
</v-btn>
</div>
</div>
<div class="patient-info-text">
<div class="patient-barcode">{{ patient.barcode }}</div>
<div class="patient-barcode-rm d-flex align-center">
<div class="patient-barcode">{{ patient.barcode }}</div>
<v-chip v-if="patient.noRM" size="x-small" color="primary-600" variant="flat" class="text-white ml-2">
RM: {{ patient.noRM }}
</v-chip>
</div>
<div class="patient-klinik-pembayaran">{{ patient.klinik }} | {{ patient.pembayaran }}</div>
</div>
@@ -299,13 +288,23 @@ defineEmits(['action', 'change-klinik', 'process-next', 'call', 'open-klinik-rua
}
.action-icon-btn {
min-width: 48px !important;
width: 48px !important;
height: 36px !important;
padding: 0 !important;
border-radius: 18px !important;
min-width: auto !important;
width: auto !important;
height: 32px !important;
padding: 0 12px !important;
border-radius: 16px !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
transition: all 0.2s ease;
text-transform: none !important;
display: flex !important;
align-items: center !important;
gap: 4px;
}
.btn-label {
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.action-icon-btn:hover {
@@ -474,9 +473,9 @@ defineEmits(['action', 'change-klinik', 'process-next', 'call', 'open-klinik-rua
}
.action-icon-btn {
flex: 1 1 calc(50% - 3px);
flex: 1 1 auto;
min-width: 0 !important;
max-width: calc(50% - 3px);
max-width: none !important;
}
}
+223 -260
View File
@@ -66,34 +66,26 @@
<div class="current-patient-header mb-2">
<div class="section-label-small">SEDANG DIPROSES</div>
<div class="action-icon-buttons">
<v-tooltip text="Pending" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="danger-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'pending')"
>
<v-icon size="18">mdi-pause</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip text="Selesai" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
class="action-icon-btn text-white"
color="success-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'selesai')"
>
<v-icon size="18">mdi-check</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-btn
class="action-icon-btn text-white"
color="danger-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'pending')"
>
<v-icon size="16">mdi-pause</v-icon>
<span class="btn-label">Pending</span>
</v-btn>
<v-btn
class="action-icon-btn text-white"
color="success-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'selesai')"
>
<v-icon size="16">mdi-check</v-icon>
<span class="btn-label">Selesai</span>
</v-btn>
</div>
</div>
<div class="current-patient-info">
@@ -177,91 +169,95 @@
<template v-slot:activator="{ props }">
<div
v-bind="props"
:class="getPatientCardClass(patient)"
:class="getPatientCardClass(patient, ruang)"
>
<!-- Left Section: Number and Status -->
<div class="patient-card-left">
<div class="patient-queue-number-wrapper">
<div class="patient-queue-number">
{{ patient.noAntrian?.split(" |")[0] || patient.barcode || '-' }}
</div>
<div class="patient-status-icons">
<div
v-if="patient.status === 'pending'"
class="status-icon-wrapper status-icon-pending"
>
<v-icon
color="var(--color-danger-600)"
size="14"
class="status-icon"
>
mdi-pause
</v-icon>
<!-- Top Row: Number, Status Icons, and Actions -->
<div class="patient-card-header-row">
<div class="patient-card-left-group">
<div class="patient-queue-number-wrapper">
<div class="patient-queue-number">
{{ patient.noAntrian?.split(" |")[0] || patient.barcode || '-' }}
</div>
<div class="patient-status-icons">
<div
v-if="patient.status === 'pending'"
class="status-icon-wrapper status-icon-pending"
>
<v-icon
color="var(--color-danger-600)"
size="14"
class="status-icon"
>
mdi-pause
</v-icon>
</div>
<div
v-if="(patient.fastTrack ?? '').toString().trim().toUpperCase() === 'YA'"
class="status-icon-wrapper status-icon-fast-track"
>
<v-icon
color="#E53935"
size="14"
class="status-icon"
>
mdi-flash
</v-icon>
</div>
</div>
</div>
</div>
<div
v-if="(patient.fastTrack ?? '').toString().trim().toUpperCase() === 'YA'"
class="status-icon-wrapper status-icon-fast-track"
>
<v-icon
color="#E53935"
size="14"
class="status-icon"
<div class="patient-card-right-group">
<div class="patient-queue-actions">
<v-btn
icon
size="small"
variant="text"
@click="showPatientDetail(patient)"
>
<v-icon size="18">mdi-eye</v-icon>
<v-tooltip activator="parent">Lihat Detail</v-tooltip>
</v-btn>
<v-btn
v-if="patient.no !== getCurrentProcessingForRoom(ruang)?.no"
class="text-white"
color="success-600"
variant="flat"
size="small"
@click="patient.status === 'pending' ? handleProcessPendingPatient(ruang, patient) : handleProcessPatient(ruang, patient)"
>
<v-icon start size="16">mdi-play</v-icon>
Proses
</v-btn>
</div>
</div>
</div>
<!-- Bottom Row: Status Chips -->
<div class="patient-card-bottom-row" v-if="patient.calledPemeriksaanAwal || patient.calledTindakan">
<div class="patient-queue-status-chips">
<v-chip
v-if="patient.calledPemeriksaanAwal"
size="small"
color="primary-600"
variant="flat"
class="status-chip status-chip-pawal"
>
mdi-flash
</v-icon>
<v-icon start size="16">mdi-check-circle</v-icon>
P.Awal
</v-chip>
<v-chip
v-if="patient.calledTindakan"
size="small"
color="secondary-600"
variant="flat"
class="status-chip status-chip-tindakan"
>
<v-icon start size="16">mdi-check-circle</v-icon>
Tindakan
</v-chip>
</div>
</div>
</div>
<div class="patient-queue-status-chips">
<!-- Compact status indicators - only show icons if called -->
<v-chip
v-if="patient.calledPemeriksaanAwal"
size="small"
color="primary-600"
variant="flat"
class="status-chip status-chip-pawal"
>
<v-icon start size="16">mdi-check-circle</v-icon>
P.Awal
</v-chip>
<v-chip
v-if="patient.calledTindakan"
size="small"
color="secondary-600"
variant="flat"
class="status-chip status-chip-tindakan"
>
<v-icon start size="16">mdi-check-circle</v-icon>
Tindakan
</v-chip>
</div>
</div>
<!-- Right Section: Actions -->
<div class="patient-card-right">
<!-- Action Buttons -->
<div class="patient-queue-actions">
<v-btn
icon
size="small"
variant="text"
@click="showPatientDetail(patient)"
>
<v-icon size="18">mdi-eye</v-icon>
<v-tooltip activator="parent">Lihat Detail</v-tooltip>
</v-btn>
<v-btn
class="text-white"
color="success-600"
variant="flat"
size="small"
@click="patient.status === 'pending' ? handleProcessPendingPatient(ruang, patient) : handleProcessPatient(ruang, patient)"
>
<v-icon start size="16">mdi-play</v-icon>
Proses
</v-btn>
</div>
</div>
</div>
</template>
</v-tooltip>
@@ -494,46 +490,36 @@
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="ruang-list">
<div class="ruang-selection-grid p-2">
<div
v-for="ruang in klinikRuang.ruangList"
:key="ruang.nomorRuang"
class="ruang-item"
class="ruang-selection-card"
:class="{
'ruang-item-selected': selectedKlinikRuangBaru?.kodeKlinik === klinikRuang.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === ruang.nomorRuang,
'ruang-item-disabled': klinikRuang.kodeKlinik === selectedPatientForKelola?.kodeKlinik && ruang.nomorRuang === selectedPatientForKelola?.nomorRuang
'ruang-selection-card-selected': selectedKlinikRuangBaru?.kodeKlinik === klinikRuang.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === ruang.nomorRuang,
'ruang-selection-card-disabled': klinikRuang.kodeKlinik === selectedPatientForKelola?.kodeKlinik && ruang.nomorRuang === selectedPatientForKelola?.nomorRuang
}"
@click="!(klinikRuang.kodeKlinik === selectedPatientForKelola?.kodeKlinik && ruang.nomorRuang === selectedPatientForKelola?.nomorRuang) && (selectedKlinikRuangBaru = { kodeKlinik: klinikRuang.kodeKlinik, namaKlinik: klinikRuang.namaKlinik, nomorRuang: ruang.nomorRuang, namaRuang: ruang.namaRuang, nomorScreen: ruang.nomorScreen })"
>
<v-icon color="secondary-600" size="20">mdi-door</v-icon>
<div class="ruang-info">
<span class="body-3 text-semibold">Ruang {{ ruang.nomorRuang }} - {{ ruang.namaRuang }}</span>
<span class="caption-2 text-muted">Screen: {{ ruang.nomorScreen }}</span>
</div>
<div class="ruang-actions">
<div class="ruang-selection-content">
<div class="ruang-selection-name">{{ ruang.namaRuang }}</div>
<div class="ruang-selection-number">Ruang {{ ruang.nomorRuang }}</div>
<v-chip
v-if="klinikRuang.kodeKlinik === selectedPatientForKelola?.kodeKlinik && ruang.nomorRuang === selectedPatientForKelola?.nomorRuang"
size="x-small"
color="neutral-500"
variant="outlined"
class="mr-2"
class="mt-2"
>
Saat Ini
</v-chip>
<v-icon
v-if="selectedKlinikRuangBaru?.kodeKlinik === klinikRuang.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === ruang.nomorRuang"
size="20"
class="mt-2"
size="18"
color="primary-600"
>
mdi-check-circle
</v-icon>
<v-icon
v-else
size="20"
color="neutral-600"
>
mdi-chevron-right
</v-icon>
</div>
</div>
</div>
@@ -592,18 +578,24 @@
<v-divider />
<v-card-text class="dialog-content" v-if="selectedPatientDetail">
<v-row dense class="detail-row">
<v-col cols="6">
<v-col cols="4">
<div class="detail-item">
<span class="detail-label">Nomor Antrian</span>
<span class="detail-value">{{ selectedPatientDetail.noAntrian?.split(" |")[0] || '-' }}</span>
</div>
</v-col>
<v-col cols="6">
<v-col cols="4">
<div class="detail-item">
<span class="detail-label">Barcode</span>
<span class="detail-value">{{ selectedPatientDetail.barcode || '-' }}</span>
</div>
</v-col>
<v-col cols="4">
<div class="detail-item">
<span class="detail-label">No. RM</span>
<span class="detail-value">{{ selectedPatientDetail.noRM || '-' }}</span>
</div>
</v-col>
</v-row>
<v-row dense class="detail-row">
<v-col cols="6">
@@ -922,8 +914,6 @@ const getAllPatientsForRoom = (ruang) => {
p.nomorRuang === ruang.nomorRuang &&
p.ruang === ruang.namaRuang &&
p.processStage === 'klinik-ruang' &&
// Exclude pasien yang sedang diproses (current processing)
p.no !== getCurrentProcessingForRoom(ruang)?.no &&
// Include semua pasien dengan status yang relevan
(p.status === 'waiting' || p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending')
);
@@ -1705,15 +1695,19 @@ const paginatedAllPatients = (ruang) => {
};
// Helper untuk menentukan class card berdasarkan status pasien
const getPatientCardClass = (patient) => {
const getPatientCardClass = (patient, ruang) => {
const baseClass = 'patient-queue-item';
const isFastTrack = (patient.fastTrack ?? '').toString().trim().toUpperCase() === 'YA';
const isProcessing = patient.no === getCurrentProcessingForRoom(ruang)?.no;
// Jika pasien fast track, tambahkan class fast track
// Tidak perlu mengubah warna card jika sudah pemeriksaan awal/tindakan
return isFastTrack
? `${baseClass} patient-queue-item-default patient-queue-item-fast-track`
: `${baseClass} patient-queue-item-default`;
let dynamicClass = 'patient-queue-item-default';
if (isProcessing) {
dynamicClass = 'patient-queue-item-processing';
} else if (isFastTrack) {
dynamicClass = 'patient-queue-item-fast-track';
}
return `${baseClass} ${dynamicClass}`;
};
@@ -1984,22 +1978,32 @@ onMounted(() => {
}
.action-icon-btn {
min-width: 40px !important;
width: 40px !important;
height: 40px !important;
padding: 0 !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
transition: all 0.2s ease !important;
min-width: auto !important;
width: auto !important;
height: 32px !important;
padding: 0 12px !important;
border-radius: 16px !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
transition: all 0.2s ease;
text-transform: none !important;
display: flex !important;
align-items: center !important;
gap: 4px;
}
.action-icon-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
.btn-label {
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.action-icon-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
.action-icon-btn:disabled {
background-color: #c59494 !important;
color: #ffffff !important;
opacity: 0.8;
cursor: not-allowed;
box-shadow: none !important;
transform: none !important;
}
.action-grid .v-btn {
@@ -2015,17 +2019,15 @@ onMounted(() => {
transition: all 0.2s ease;
}
.action-grid .v-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
.action-grid .v-btn:disabled {
background-color: var(--color-neutral-700) !important;
color: var(--color-neutral-100) !important;
opacity: 0.8 !important;
box-shadow: none !important;
transform: none !important;
}
.action-grid .v-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.empty-current {
.empty-current {
text-align: center;
padding: 24px 16px;
background: var(--color-neutral-200);
@@ -2284,17 +2286,43 @@ onMounted(() => {
.patient-queue-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px 20px;
flex-direction: column;
padding: 12px 20px;
border-radius: 8px;
transition: all 0.2s ease;
gap: 24px;
gap: 4px;
min-height: auto;
width: 100%;
box-sizing: border-box;
}
.patient-card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 16px;
}
.patient-card-left-group {
display: flex;
align-items: center;
flex: 0 0 auto;
}
.patient-card-right-group {
display: flex;
align-items: center;
flex: 0 0 auto;
justify-content: flex-end;
}
.patient-card-bottom-row {
width: 100%;
display: flex;
justify-content: flex-start;
}
/* Pasien default */
.patient-queue-item-default {
@@ -2307,6 +2335,16 @@ onMounted(() => {
border-color: var(--color-neutral-400);
}
/* Pasien sedang diproses */
.patient-queue-item-processing {
background: linear-gradient(135deg, var(--color-primary-50) 0%, var(--color-primary-100) 100%) !important;
border: 1px solid var(--color-primary-400) !important;
}
.patient-queue-item-processing:hover {
background: linear-gradient(135deg, var(--color-primary-100) 0%, var(--color-primary-200) 100%) !important;
}
/* Pasien fast track - harus setelah processed dan default agar bisa override */
.patient-queue-item-fast-track {
background: #FBE9E8 !important;
@@ -2331,13 +2369,7 @@ onMounted(() => {
/* Left Section: Number and Status */
.patient-card-left {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 160px;
max-width: 200px;
flex-shrink: 0;
flex: 0 0 auto;
display: none;
}
.patient-queue-number-wrapper {
@@ -2456,14 +2488,7 @@ onMounted(() => {
/* Right Section: Actions */
.patient-card-right {
display: flex;
align-items: flex-start;
flex: 1;
justify-content: flex-end;
min-width: 400px;
max-width: none;
padding-top: 2px;
box-sizing: border-box;
display: none;
}
.patient-queue-actions {
@@ -2545,17 +2570,16 @@ onMounted(() => {
}
.patient-queue-item {
padding: 14px 16px;
gap: 20px;
padding: 10px 16px;
gap: 4px;
}
.patient-card-left {
min-width: 150px;
max-width: 180px;
.patient-card-left-group {
min-width: 140px;
}
.patient-card-right {
min-width: 360px;
.patient-card-right-group {
min-width: 300px;
}
.patient-queue-actions {
@@ -2602,9 +2626,7 @@ onMounted(() => {
}
.action-icon-btn {
min-width: 36px !important;
width: 36px !important;
height: 36px !important;
height: 32px !important;
}
}
@@ -2616,23 +2638,20 @@ onMounted(() => {
}
.patient-queue-item {
flex-direction: row;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
gap: 2px;
}
.patient-card-left {
min-width: 100px;
flex: 0 0 auto;
.patient-card-header-row {
gap: 8px;
}
.patient-card-left-group {
min-width: auto;
}
.patient-card-right {
width: auto;
justify-content: flex-end;
padding-top: 2px;
flex: 1;
min-width: 300px;
max-width: 100%;
.patient-card-right-group {
min-width: auto;
}
.patient-queue-actions {
@@ -2672,8 +2691,6 @@ onMounted(() => {
}
.action-icon-btn {
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
}
@@ -3051,63 +3068,9 @@ onMounted(() => {
align-items: center;
}
.ruang-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.ruang-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-neutral-300);
border: 1px solid var(--color-neutral-400);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.ruang-item:hover:not(.ruang-item-disabled) {
border-color: var(--color-primary-400);
background: rgba(58, 97, 201, 0.1);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.ruang-item:active:not(.ruang-item-disabled) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.ruang-item-selected {
background: var(--color-primary-100);
border-color: var(--color-primary-600);
box-shadow: 0 2px 8px rgba(58, 97, 201, 0.2);
}
.ruang-item-disabled {
background: var(--color-neutral-200);
border-color: var(--color-neutral-400);
cursor: not-allowed;
opacity: 0.6;
}
.ruang-item-disabled:hover {
transform: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border-color: var(--color-neutral-400);
background: var(--color-neutral-200);
}
.ruang-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
/* Ruang List replacement with common grid styles */
.expansion-panel-text :deep(.v-expansion-panel-text__wrapper) {
padding: 8px 16px 16px !important;
}
.ruang-actions {
+2 -4
View File
@@ -181,8 +181,6 @@
>
<v-icon color="warning" size="24" class="mb-2">mdi-door</v-icon>
<div class="ruang-card-name">Ruang {{ ruang.nomorRuang }}</div>
<div class="ruang-card-detail">{{ ruang.namaRuang }}</div>
<div class="ruang-card-screen">Screen: {{ ruang.nomorScreen }}</div>
</div>
</div>
</v-expansion-panel-text>
@@ -757,14 +755,14 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px 12px;
padding: 12px 8px;
background: var(--color-neutral-100);
border: 1px solid var(--color-neutral-400);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
min-height: 120px;
min-height: 80px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
+89 -107
View File
@@ -175,21 +175,22 @@
<!-- Section Info Dokter untuk Non-Eksekutif (hanya tampil) -->
<div v-else class="doctor-list-section">
<p class="mb-3"><strong>Dokter yang Tersedia:</strong></p>
<div class="doctor-info-grid">
<p class="mb-2"><strong>Dokter yang Tersedia:</strong></p>
<div v-if="dialogDoctors.length > 0" class="doctor-info-unified">
<div
v-for="(doctor, index) in dialogDoctors"
:key="index"
class="doctor-info-item"
class="doctor-info-unit"
>
<div class="doctor-info-card">
<v-icon size="16" color="primary-600" class="doctor-info-icon">
mdi-doctor
</v-icon>
<span class="doctor-info-name">{{ doctor }}</span>
</div>
<v-icon size="16" color="primary-600" class="mr-2 mt-1">
mdi-doctor
</v-icon>
<span class="doctor-info-name-unified">{{ doctor }}</span>
</div>
</div>
<p v-else class="text-error text-caption italic">
Tidak ada dokter tersedia untuk klinik ini
</p>
</div>
<div class="quota-section">
@@ -672,6 +673,7 @@
<script setup>
import { ref, computed, onMounted, nextTick, watch } from "vue";
import { useRoute } from "#app";
import { storeToRefs } from "pinia";
import { useAnjunganStore } from "@/stores/anjunganStore";
import { useClinicStore } from "@/stores/clinicStore";
import { useQueueStore } from "@/stores/queueStore";
@@ -687,6 +689,10 @@ const clinicStore = useClinicStore();
const queueStore = useQueueStore();
const { printTicketFromPatient, isPrinting } = useThermalPrint();
// Gunakan storeToRefs untuk memastikan reaktivitas yang stabil terutama saat cold start/refresh
const { anjunganItems } = storeToRefs(anjunganStore);
const { getAllClinics } = storeToRefs(clinicStore);
// Validasi bahwa queueStore ter-load dengan benar
onMounted(() => {
console.log('🔍 QueueStore initialized:', queueStore);
@@ -705,23 +711,12 @@ const anjunganId = computed(() => {
});
const anjunganData = computed(() => {
const id = anjunganId.value;
if (!id) return null;
const idValue = anjunganId.value;
if (!idValue) return null;
const fromGetter =
typeof anjunganStore.getAnjunganById === "function"
? anjunganStore.getAnjunganById(id)
: null;
if (fromGetter?.value) {
return fromGetter.value;
}
const list =
anjunganStore.anjunganItems?.value || anjunganStore.anjunganItems || [];
if (!Array.isArray(list)) return null;
return list.find((a) => Number(a.id) === Number(id)) || null;
// Langsung cari di anjunganItems yang sudah di-storeToRefs
const list = anjunganItems.value || [];
return list.find((a) => Number(a.id) === Number(idValue)) || null;
});
const doctorsByKlinikId = ref({});
@@ -787,14 +782,11 @@ const prefetchDoctorsForCurrentAnjungan = async () => {
return;
}
// Get all clinics for this anjungan
const allClinics = clinicStore.getAllClinics;
const clinicsForAnjungan = anjunganData.value.klinik
.map((kode) => allClinics.find((c) => c.kode === kode))
.filter(Boolean);
// Get all matched clinics for this anjungan (already filtered by type)
const clinicsForAnjungan = filteredClinics.value;
console.log(
`🔄 Prefetching doctors for ${clinicsForAnjungan.length} klinik`
`🔄 Prefetching doctors for ${clinicsForAnjungan.length} klinik (${anjunganData.value.jenisPasien})`
);
await Promise.all(clinicsForAnjungan.map((clinic) => loadDoctorsForClinic(clinic)));
@@ -825,33 +817,33 @@ const dialogDoctors = computed(() => {
});
onMounted(async () => {
console.log("🚀 Component mounted, starting data load...");
// Wait for computed values to be ready
console.log("🚀 Component mounted");
await nextTick();
// Prefetch doctors for all clinics in this anjungan
await prefetchDoctorsForCurrentAnjungan();
console.log("✅ Initial data load complete");
console.log("✅ Initial mount complete");
});
const filteredClinics = computed(() => {
if (
!anjunganData.value ||
!anjunganData.value.klinik ||
anjunganData.value.klinik.length === 0
) {
return [];
}
if (!anjunganData.value || !anjunganData.value.klinik) return [];
const allClinics = clinicStore.getAllClinics;
const targetJenis = anjunganData.value.jenisPasien;
const targetKodes = anjunganData.value.klinik;
return anjunganData.value.klinik
.map((kode) => allClinics.find((c) => c.kode === kode) || null)
return targetKodes
.map((kode) => getAllClinics.value.find((c) => c.kode === kode && c.jenisLayanan === targetJenis) || null)
.filter(Boolean);
});
// Watcher untuk anjunganData: Jika baru terisi setelah mount, pastikan prefetch berjalan
watch(anjunganData, async (newVal) => {
if (newVal) {
console.log("🎯 anjunganData changed/ready, performing pre-checks...");
if (newVal.jenisPasien === 'Reguler') {
await clinicStore.fetchRegulerClinics();
}
await prefetchDoctorsForCurrentAnjungan();
}
}, { immediate: true });
const showVisitTypeDialog = ref(false);
const showBookingFormDialog = ref(false);
const showDoctorSelectionDialog = ref(false);
@@ -1622,78 +1614,68 @@ watch(lastRegisteredPatient, (newVal) => {
}
}
.doctor-info-grid {
.doctor-info-unified {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 8px;
}
.doctor-info-item {
width: 100%;
}
.doctor-info-card {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 10px;
background: var(--color-primary-100);
grid-template-columns: repeat(4, 1fr);
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-300);
border-radius: 8px;
border: 1px solid var(--color-primary-300);
min-height: 50px;
}
.doctor-info-icon {
flex-shrink: 0;
}
.doctor-info-name {
flex: 1;
text-align: left;
font-size: 13px;
line-height: 1.3;
color: var(--color-neutral-800);
white-space: normal;
word-wrap: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 20px;
}
.doctor-info-unit {
display: flex;
align-items: flex-start;
padding: 12px 14px;
border-bottom: 1px solid var(--color-neutral-200);
border-right: 1px solid var(--color-neutral-200);
}
.doctor-info-name-unified {
font-size: 13px;
line-height: 1.4;
color: var(--color-neutral-800);
word-break: break-word;
}
/* Responsive & Separator Logic */
@media (min-width: 961px) {
.doctor-info-unit:nth-child(4n) {
border-right: none;
}
}
@media (max-width: 960px) {
.doctor-info-unified {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 960px) and (min-width: 769px) {
.doctor-info-unit:nth-child(3n) {
border-right: none;
}
}
/* Responsive untuk layar lebih kecil - doctor info grid */
@media (max-width: 768px) {
.doctor-info-grid {
.doctor-info-unified {
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
}
.doctor-info-card {
padding: 10px 8px;
min-height: 45px;
}
.doctor-info-name {
font-size: 12px;
@media (max-width: 768px) and (min-width: 481px) {
.doctor-info-unit:nth-child(2n) {
border-right: none;
}
}
@media (max-width: 480px) {
.doctor-info-grid {
grid-template-columns: repeat(2, 1fr);
gap: 6px;
.doctor-info-unified {
grid-template-columns: 1fr;
}
.doctor-info-card {
padding: 10px 6px;
min-height: 45px;
}
.doctor-info-name {
font-size: 11px;
.doctor-info-unit {
border-right: none;
}
}
+109 -136
View File
@@ -721,144 +721,117 @@ export const useClinicStore = defineStore('clinic', () => {
return result;
};
// Fetch Reguler clinics from API
const fetchRegulerClinics = async () => {
isLoadingAPI.value = true;
apiError.value = null;
// State for singleton fetch promise
let activeFetchPromise = null;
try {
console.log('🔄 Fetching clinics from API...');
const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/reguler');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const rawData = await response.json();
console.log('📦 Raw API Response:', rawData);
console.log('📦 Response type:', typeof rawData);
console.log('📦 Is Array?', Array.isArray(rawData));
// Handle different response formats
let data = rawData;
// If data is wrapped in an object (e.g., { data: [...], status: 200 })
if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {
console.log('📦 Response is an object, checking for data property...');
if (rawData.data) {
data = rawData.data;
console.log('✅ Found data property:', data);
} else if (rawData.result) {
data = rawData.result;
console.log('✅ Found result property:', data);
} else if (rawData.response) {
data = rawData.response;
console.log('✅ Found response property:', data);
}
}
console.log('📊 Processed data:', data);
console.log('📊 Processed data is Array?', Array.isArray(data));
if (!data || !Array.isArray(data)) {
console.error('❌ Data is not an array after processing');
console.error('❌ Data type:', typeof data);
console.error('❌ Data value:', data);
return {
success: false,
message: `Data format tidak valid. Expected array, got ${typeof data}. Check console for details.`
};
}
console.log(`✅ Found ${data.length} items in API response`);
// Log first item for debugging
if (data.length > 0) {
console.log('📋 First item sample:', JSON.stringify(data[0], null, 2));
}
// Remove existing Reguler clinics
clinics.value = clinics.value.filter(c => c.jenisLayanan !== 'Reguler');
console.log(`🗑️ Removed existing Reguler clinics. Remaining: ${clinics.value.length}`);
// Filter and map clinics
const filteredData = data.filter(item => {
const isReguler = item.Tipe_Anjungan === 'KLINIK REGULER';
if (!isReguler) {
console.log(`⏭️ Skipping ${item.namaklinik} - Tipe: ${item.Tipe_Anjungan}`);
}
return isReguler;
});
console.log(`✅ Filtered ${filteredData.length} KLINIK REGULER items`);
// Add new Reguler clinics from API
const regulerClinics = filteredData.map((item, index) => {
console.log(`\n🏥 Processing clinic ${index + 1}/${filteredData.length}:`, item.namaklinik);
console.log(' - ID:', item.idklinik);
console.log(' - Code:', item.code);
console.log(' - Active:', item.active);
console.log(' - Jadwal:', item.jadwal);
const parsed = parseJamOperasional(item.jadwal);
console.log(' - Parsed jamShiftPerHari:', parsed.jamShiftPerHari);
console.log(' - Total quota:', parsed.totalQuota);
console.log(' - Jadwal klinik:', parsed.jadwalKlinik);
// Get first shift from first day for display purposes
const firstDay = parsed.jadwalKlinik[0];
const firstShift = firstDay ? parsed.jamShiftPerHari[firstDay]?.[0] : null;
return {
id: item.idklinik,
kode: item.code || 'XX',
name: item.namaklinik || 'Unknown',
subtitle: "",
icon: "mdi-hospital-box",
shift: `${parsed.shift} SHIFT`,
schedule: `Mulai Pukul ${firstShift?.dari || '07:00'}`,
available: item.active,
doctors: [],
shifts: [], // Legacy field, kept for compatibility
totalQuota: parsed.totalQuota,
jamShiftPerHari: parsed.jamShiftPerHari, // NEW: shift configuration per day
jamShiftList: [], // Legacy field, kept for compatibility
autoShift: false,
jadwalKlinik: parsed.jadwalKlinik,
tanggalTutup: [], // NEW: array of closed dates
jenisLayanan: "Reguler",
};
});
console.log(`✅ Created ${regulerClinics.length} clinic objects`);
// Add reguler clinics to the list
clinics.value = [...clinics.value, ...regulerClinics];
console.log(`✅ Total clinics after merge: ${clinics.value.length}`);
// Sort by kode, then jenisLayanan
clinics.value.sort((a, b) => {
if (a.kode < b.kode) return -1;
if (a.kode > b.kode) return 1;
if (a.jenisLayanan === 'Reguler' && b.jenisLayanan === 'Eksekutif') return -1;
if (a.jenisLayanan === 'Eksekutif' && b.jenisLayanan === 'Reguler') return 1;
return a.id - b.id;
});
console.log('✅ Clinics sorted by kode and jenisLayanan');
console.log('🎉 Final clinic list:', clinics.value.map(c => ({ id: c.id, name: c.name, jenisLayanan: c.jenisLayanan })));
return { success: true, message: `${regulerClinics.length} klinik reguler berhasil dimuat` };
} catch (error) {
console.error('❌ Error fetching reguler clinics:', error);
console.error('❌ Error stack:', error.stack);
apiError.value = error.message;
return { success: false, message: `Gagal memuat data: ${error.message}` };
} finally {
isLoadingAPI.value = false;
console.log('✅ API fetch completed');
// Fetch Reguler clinics from API
const fetchRegulerClinics = async (force = false, retryCount = 0) => {
// Guard: singleton pattern - reuse existing promise if one is already in flight
if (activeFetchPromise && retryCount === 0) {
console.log('⏳ Clinic fetch in progress, reusing existing request...');
return activeFetchPromise;
}
// Guard: Skip if data already exists and not forced
const hasRegulerClinics = clinics.value.some(c => c.jenisLayanan === 'Reguler');
if (hasRegulerClinics && !force && retryCount === 0) {
return { success: true, message: 'Data sudah tersedia' };
}
// Define the actual fetch operation
const performFetch = async () => {
isLoadingAPI.value = true;
apiError.value = null;
try {
console.log(`🔄 Fetching clinics from API (Try ${retryCount + 1})...`);
const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/reguler');
if (!response.ok) {
// Handle Rate Limiting with exponential backoff
if (response.status === 429 && retryCount < 3) {
const delay = (retryCount + 1) * 1500;
console.warn(`⚠️ Rate limit hit (429). Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
activeFetchPromise = null; // Reset so next try can start fresh
return fetchRegulerClinics(force, retryCount + 1);
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const rawData = await response.json();
console.log('📦 Raw API Response received');
// Handle different response formats
let data = rawData;
if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {
if (rawData.data) data = rawData.data;
else if (rawData.result) data = rawData.result;
else if (rawData.response) data = rawData.response;
}
if (!data || !Array.isArray(data)) {
throw new Error('Data format tidak valid. Expected array.');
}
// Remove existing Reguler clinics
clinics.value = clinics.value.filter(c => c.jenisLayanan !== 'Reguler');
// Filter and map clinics
const regulerClinics = data
.filter(item => item.Tipe_Anjungan === 'KLINIK REGULER')
.map((item) => {
const parsed = parseJamOperasional(item.jadwal);
const firstDay = parsed.jadwalKlinik[0];
const firstShift = firstDay ? parsed.jamShiftPerHari[firstDay]?.[0] : null;
return {
id: item.idklinik,
kode: item.code || 'XX',
name: item.namaklinik || 'Unknown',
subtitle: "",
icon: "mdi-hospital-box",
shift: `${parsed.shift} SHIFT`,
schedule: `Mulai Pukul ${firstShift?.dari || '07:00'}`,
available: item.active,
doctors: [],
shifts: [],
totalQuota: parsed.totalQuota,
jamShiftPerHari: parsed.jamShiftPerHari,
jamShiftList: [],
autoShift: false,
jadwalKlinik: parsed.jadwalKlinik,
tanggalTutup: [],
jenisLayanan: "Reguler",
};
});
clinics.value = [...clinics.value, ...regulerClinics];
// Sort by kode, then jenisLayanan
clinics.value.sort((a, b) => {
if (a.kode < b.kode) return -1;
if (a.kode > b.kode) return 1;
if (a.jenisLayanan === 'Reguler' && b.jenisLayanan === 'Eksekutif') return -1;
if (a.jenisLayanan === 'Eksekutif' && b.jenisLayanan === 'Reguler') return 1;
return a.id - b.id;
});
return { success: true, message: `${regulerClinics.length} klinik reguler berhasil dimuat` };
} catch (error) {
console.error('❌ Error fetching reguler clinics:', error);
apiError.value = error.message;
return { success: false, message: `Gagal memuat data: ${error.message}` };
} finally {
isLoadingAPI.value = false;
activeFetchPromise = null; // Clear singleton once finished
console.log('✅ API fetch flow completed');
}
};
activeFetchPromise = performFetch();
return activeFetchPromise;
};
// Get all clinics (sorted by kode first, then by jenisLayanan)
+20
View File
@@ -207,6 +207,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000001",
penanggungJawab: "Dr. Ahmad Wijaya", // Fast Track data
alasanFastTrack: "Pasien prioritas", // Fast Track data
},
@@ -226,6 +227,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000002",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -245,6 +247,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000003",
penanggungJawab: "Dr. Budi Santoso", // Fast Track data
alasanFastTrack: "Kondisi darurat", // Fast Track data
},
@@ -264,6 +267,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000004",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -283,6 +287,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000005",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -302,6 +307,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000006",
penanggungJawab: "Dr. Citra Dewi", // Fast Track data
alasanFastTrack: "Rujukan darurat", // Fast Track data
},
@@ -321,6 +327,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000007",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -340,6 +347,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000008",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -359,6 +367,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000009",
penanggungJawab: "Dr. Dedi Kurniawan", // Fast Track data
alasanFastTrack: "Pasien VIP", // Fast Track data
},
@@ -378,6 +387,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000010",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -397,6 +407,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000011",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -416,6 +427,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: null,
noRM: "RM-000012",
penanggungJawab: "Dr. Eka Putri", // Fast Track data
alasanFastTrack: "Kondisi kritis", // Fast Track data
},
@@ -435,6 +447,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: "Dr. Ahmad Wijaya, Sp.OG",
noRM: "RM-000013",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -454,6 +467,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: "Dr. Budi Santoso, Sp.PD",
noRM: "RM-000014",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -473,6 +487,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: "Dr. Citra Dewi, Sp.S",
noRM: "RM-000015",
penanggungJawab: "Dr. Citra Dewi", // Fast Track data
alasanFastTrack: "Pasien Eksekutif prioritas", // Fast Track data
},
@@ -492,6 +507,7 @@ export const useQueueStore = defineStore('queue', () => {
visitType: 'SEKARANG',
visitDate: new Date().toISOString().substring(0, 10),
namaDokter: "Dr. Dedi Kurniawan, Sp.THT",
noRM: "RM-000016",
penanggungJawab: null,
alasanFastTrack: null,
},
@@ -939,6 +955,7 @@ export const useQueueStore = defineStore('queue', () => {
klinik: klinik.name,
fastTrack: "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
noRM: patient ? (patient.noRM || `RM-SCAN-${barcode.slice(-6)}`) : `RM-SCAN-${barcode.slice(-6)}`,
status: "di-loket",
processStage: "klinik",
createdAt: timestamp.toISOString(),
@@ -975,6 +992,7 @@ export const useQueueStore = defineStore('queue', () => {
klinik: penunjang.name,
fastTrack: "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
noRM: patient ? patient.noRM : `RM-SCAN-${barcode.slice(-6)}`,
status: "di-loket",
processStage: "penunjang",
createdAt: timestamp.toISOString(),
@@ -1038,6 +1056,7 @@ export const useQueueStore = defineStore('queue', () => {
nomorScreen: ruang.nomorScreen,
fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
noRM: patient ? patient.noRM : null,
status: "waiting",
processStage: "klinik-ruang", // Set ke klinik-ruang langsung
createdAt: timestamp.toISOString(),
@@ -1561,6 +1580,7 @@ export const useQueueStore = defineStore('queue', () => {
klinik: clinic.name || clinic,
fastTrack: isFastTrack ? "YA" : "TIDAK",
pembayaran: paymentType,
noRM: `RM-${barcode.slice(-6)}`, // Generate No. RM from barcode suffix
status: status,
processStage: "loket",
createdAt: timestamp.toISOString(),