update no rm dan desain
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user