update flow klinik ruang, dan design consistency

This commit is contained in:
bagus-arie05
2026-01-19 10:11:09 +07:00
parent 00bb95431c
commit cbd8f44b50
9 changed files with 809 additions and 231 deletions
+288 -91
View File
@@ -62,43 +62,66 @@
<v-card-text class="pa-4">
<!-- Current Processing Patient -->
<div v-if="getCurrentProcessingForRoom(ruang)" class="current-patient-section mb-4">
<div class="section-label-small mb-2">SEDANG DIPROSES</div>
<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>
</div>
</div>
<div class="current-patient-info">
<div class="patient-number-large">
{{ getCurrentProcessingForRoom(ruang)?.noAntrian?.split(" |")[0] || '-' }}
</div>
<div class="patient-details">
<div class="action-grid">
<v-btn
class="text-white"
color="primary-600"
variant="flat"
size="small"
@click="handleCallPatientByTipe(ruang, 'Pemeriksaan Awal')"
>
<v-icon start size="18">mdi-bullhorn</v-icon>
Pemeriksaan Awal
</v-btn>
<v-btn
class="text-white"
color="secondary-600"
variant="flat"
size="small"
:disabled="!getCurrentProcessingForRoom(ruang)?.calledPemeriksaanAwal"
@click="handleCallPatientByTipe(ruang, 'Tindakan')"
>
<v-icon start size="18">mdi-bullhorn</v-icon>
Panggil Tindakan
</v-btn>
<v-btn
class="text-white"
color="success-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'selesai')"
>
<v-icon start size="18">mdi-check</v-icon>
Selesai
</v-btn>
<v-btn
class="text-white"
color="danger-600"
variant="flat"
size="small"
@click="handlePatientAction(ruang, 'pending')"
>
<v-icon start size="18">mdi-pause</v-icon>
Pending
</v-btn>
</div>
</div>
</div>
@@ -226,16 +249,6 @@
<v-icon size="18">mdi-eye</v-icon>
<v-tooltip activator="parent">Lihat Detail</v-tooltip>
</v-btn>
<v-btn
class="text-white call-btn"
color="primary-600"
variant="flat"
size="small"
@click="handleCallPatientFromList(ruang, patient, 'Pemeriksaan Awal')"
>
<v-icon start size="16">mdi-bullhorn</v-icon>
Pemeriksaan Awal
</v-btn>
<v-btn
class="text-white"
color="success-600"
@@ -447,30 +460,85 @@
</div>
<!-- Pilih Klinik Ruang (untuk Pindah Klinik dan Konsultasi) -->
<div v-else class="klinik-ruang-selection-list">
<div
v-for="klinikRuangOption in getAllKlinikRuangOptions()"
:key="`${klinikRuangOption.kodeKlinik}-${klinikRuangOption.nomorRuang}`"
class="klinik-ruang-selection-card"
:class="{
'klinik-ruang-selection-card-selected': selectedKlinikRuangBaru?.kodeKlinik === klinikRuangOption.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === klinikRuangOption.nomorRuang,
'klinik-ruang-selection-card-disabled': klinikRuangOption.kodeKlinik === selectedPatientForKelola?.kodeKlinik && klinikRuangOption.nomorRuang === selectedPatientForKelola?.nomorRuang
}"
@click="!(klinikRuangOption.kodeKlinik === selectedPatientForKelola?.kodeKlinik && klinikRuangOption.nomorRuang === selectedPatientForKelola?.nomorRuang) && (selectedKlinikRuangBaru = klinikRuangOption)"
>
<div class="klinik-ruang-selection-content">
<div class="klinik-ruang-selection-name">{{ klinikRuangOption.namaKlinik }}</div>
<div class="klinik-ruang-selection-room">{{ klinikRuangOption.namaRuang }} (R.{{ klinikRuangOption.nomorRuang }})</div>
<v-chip
v-if="klinikRuangOption.kodeKlinik === selectedPatientForKelola?.kodeKlinik && klinikRuangOption.nomorRuang === selectedPatientForKelola?.nomorRuang"
size="x-small"
color="neutral-500"
class="mt-2"
>
Saat Ini
</v-chip>
</div>
</div>
<div v-else>
<v-text-field
v-model="klinikRuangSearch"
placeholder="Cari Klinik Ruang..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
hide-details
class="mb-4"
clearable
/>
<v-expansion-panels class="expansion-panels">
<v-expansion-panel
v-for="klinikRuang in filteredKlinikRuang"
:key="klinikRuang.kodeKlinik"
class="expansion-panel"
>
<v-expansion-panel-title class="expansion-title">
<div class="expansion-header">
<v-chip size="small" color="secondary-600" class="mr-2">
{{ klinikRuang.kodeKlinik }}
</v-chip>
<span class="body-2 text-semibold">{{ klinikRuang.namaKlinik }}</span>
</div>
<template #actions>
<v-chip size="x-small" variant="outlined">
{{ klinikRuang.ruangList?.length || 0 }} Ruang
</v-chip>
</template>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="ruang-list">
<div
v-for="ruang in klinikRuang.ruangList"
:key="ruang.nomorRuang"
class="ruang-item"
:class="{
'ruang-item-selected': selectedKlinikRuangBaru?.kodeKlinik === klinikRuang.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === ruang.nomorRuang,
'ruang-item-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">
<v-chip
v-if="klinikRuang.kodeKlinik === selectedPatientForKelola?.kodeKlinik && ruang.nomorRuang === selectedPatientForKelola?.nomorRuang"
size="x-small"
color="neutral-500"
variant="outlined"
class="mr-2"
>
Saat Ini
</v-chip>
<v-icon
v-if="selectedKlinikRuangBaru?.kodeKlinik === klinikRuang.kodeKlinik && selectedKlinikRuangBaru?.nomorRuang === ruang.nomorRuang"
size="20"
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>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
</div>
</v-card-text>
@@ -612,8 +680,7 @@
<span class="detail-label">Fast Track</span>
<v-chip
size="small"
:color="selectedPatientDetail.fastTrack === 'YA' ? 'danger-600' : 'neutral-500'"
variant="flat"
variant="outlined"
class="detail-chip"
>
{{ selectedPatientDetail.fastTrack === 'YA' ? 'Ya' : 'Tidak' }}
@@ -801,6 +868,7 @@ const sortOptions = ref({});
const searchPatientQuery = ref('');
const showFilterDialog = ref({});
const filterOptions = ref({});
const klinikRuangSearch = ref('');
// Initialize filter options for each room
const initializeFilterOptions = (ruang) => {
@@ -1075,6 +1143,7 @@ const closeKelolaPasienDialog = () => {
selectedRuangBaru.value = null;
selectedKlinikRuangBaru.value = null;
searchPatientQuery.value = '';
klinikRuangSearch.value = '';
};
// Get all available klinik ruang options (for pindah klinik and konsultasi)
@@ -1097,6 +1166,43 @@ const getAllKlinikRuangOptions = () => {
return options;
};
// Get klinik ruang data grouped by klinik (for expansion panels)
const klinikRuangList = computed(() => {
return masterStore.ruangData || [];
});
// Filtered klinik ruang based on search query
const filteredKlinikRuang = computed(() => {
if (!klinikRuangSearch.value) return klinikRuangList.value;
const searchLower = klinikRuangSearch.value.toLowerCase();
return klinikRuangList.value
.map(klinikRuang => {
// Check if klinik name or code matches
const klinikMatches =
klinikRuang.namaKlinik?.toLowerCase().includes(searchLower) ||
klinikRuang.kodeKlinik?.toLowerCase().includes(searchLower);
// Filter ruangList based on search
const filteredRuangList = klinikRuang.ruangList?.filter(ruang =>
ruang.namaRuang?.toLowerCase().includes(searchLower) ||
ruang.nomorRuang?.toString().includes(searchLower) ||
ruang.nomorScreen?.toString().includes(searchLower)
) || [];
// Return klinik if klinik matches or has matching ruang
if (klinikMatches || filteredRuangList.length > 0) {
return {
...klinikRuang,
ruangList: klinikMatches ? klinikRuang.ruangList : filteredRuangList
};
}
return null;
})
.filter(k => k !== null && k.ruangList && k.ruangList.length > 0);
});
const confirmPindahRuang = () => {
if (!selectedRuangBaru.value || !selectedPatientForKelola.value) {
showSnackbar('Pilih ruang tujuan', 'error');
@@ -1849,11 +1955,43 @@ onMounted(() => {
margin-bottom: 4px;
}
.current-patient-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.action-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 16px;
align-items: center;
}
.action-icon-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.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;
}
.action-icon-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
}
.action-icon-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
}
.action-grid .v-btn {
@@ -2098,6 +2236,11 @@ onMounted(() => {
line-height: 24px;
}
.body-3 {
font-size: 14px;
line-height: 20px;
}
.caption-2 {
font-size: 12px;
line-height: 16px;
@@ -2436,7 +2579,7 @@ onMounted(() => {
}
.action-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 1fr 1fr;
gap: 8px;
}
@@ -2449,6 +2592,12 @@ onMounted(() => {
.action-grid .v-btn .v-icon {
font-size: 16px;
}
.action-icon-btn {
min-width: 36px !important;
width: 36px !important;
height: 36px !important;
}
}
@media (max-width: 960px) {
@@ -2500,7 +2649,7 @@ onMounted(() => {
}
.action-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 1fr 1fr;
gap: 8px;
}
@@ -2514,6 +2663,26 @@ onMounted(() => {
font-size: 14px;
}
.action-icon-btn {
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
}
.action-icon-btn .v-icon {
font-size: 16px;
}
.action-icon-btn {
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
}
.action-icon-btn .v-icon {
font-size: 16px;
}
.patient-queue-actions {
gap: 4px;
flex-wrap: nowrap;
@@ -2847,68 +3016,96 @@ onMounted(() => {
margin-bottom: 4px;
}
/* Klinik Ruang Selection Styles */
.klinik-ruang-selection-list {
max-height: 400px;
overflow-y: auto;
/* Klinik Ruang Selection Styles - Expansion Panels */
.expansion-panels {
background: transparent;
}
.expansion-panel {
background: var(--color-neutral-100);
border: 1px solid var(--color-neutral-400);
border-radius: 12px !important;
margin-bottom: 12px;
overflow: hidden;
}
.expansion-panel::before {
box-shadow: none !important;
}
.expansion-title {
background: var(--color-neutral-100);
min-height: 60px;
}
.expansion-header {
display: flex;
align-items: center;
}
.ruang-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.klinik-ruang-selection-card {
.ruang-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: var(--color-neutral-100);
border: 2px solid var(--color-neutral-300);
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);
}
.klinik-ruang-selection-card:hover:not(.klinik-ruang-selection-card-disabled) {
background: var(--color-neutral-200);
.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);
}
.klinik-ruang-selection-card-selected {
background: var(--color-primary-100);
border-color: var(--color-primary-600);
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.2);
.ruang-item:active:not(.ruang-item-disabled) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.klinik-ruang-selection-card-disabled {
.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;
}
.klinik-ruang-selection-card-disabled:hover {
.ruang-item-disabled:hover {
transform: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
border-color: var(--color-neutral-400);
background: var(--color-neutral-200);
}
.klinik-ruang-selection-content {
.ruang-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.klinik-ruang-selection-name {
font-size: 16px;
line-height: 24px;
font-weight: 600;
color: var(--color-neutral-900);
margin-bottom: 4px;
}
.klinik-ruang-selection-room {
font-size: 14px;
line-height: 20px;
color: var(--color-neutral-600);
.ruang-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* Filter and Sort Actions */
+1 -1
View File
@@ -56,7 +56,7 @@
variant="flat"
size="large"
block
class="btn-view"
class="btn-view text-white"
>
<v-icon left>mdi-eye</v-icon>
Tampilkan
@@ -92,7 +92,7 @@
<div class="footer-stats-bar">
<div class="stat-item">
<div class="stat-icon stat-icon-total">
<v-icon size="32" color="success-600">mdi-format-list-numbered</v-icon>
<v-icon size="32" color="primary-600">mdi-format-list-numbered</v-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.total }}</div>
@@ -104,7 +104,7 @@
<div class="stat-item">
<div class="stat-icon stat-icon-waiting">
<v-icon size="32" color="success-600">mdi-clock-alert-outline</v-icon>
<v-icon size="32" color="primary-600">mdi-clock-alert-outline</v-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.waiting }}</div>
@@ -116,7 +116,7 @@
<div class="stat-item">
<div class="stat-icon stat-icon-active">
<v-icon size="32" color="success-600">mdi-account-check</v-icon>
<v-icon size="32" color="primary-600">mdi-account-check</v-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.active }}</div>
@@ -125,7 +125,7 @@
</div>
<div class="footer-message">
<v-icon size="24" color="success-600" class="mr-2">mdi-information</v-icon>
<v-icon size="24" color="primary-600" class="mr-2">mdi-information</v-icon>
<span>Harap perhatikan nomor antrian Anda di layar</span>
</div>
</div>
@@ -620,11 +620,11 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, var(--color-success-600) 0%, var(--color-success-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 16px;
padding: 24px 40px;
margin-bottom: 20px;
box-shadow: 0 8px 24px rgba(0, 146, 98, 0.3);
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.3);
}
.header-left {
@@ -701,12 +701,13 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
/* ========== HERO CALL SECTION ========== */
.hero-call-section {
background: linear-gradient(135deg, var(--color-danger-600) 0%, var(--color-danger-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 32px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.35);
box-shadow: 0 12px 32px rgba(33, 150, 243, 0.35);
animation: pulse-highlight 2s ease-in-out infinite;
}
.call-label {
@@ -729,6 +730,7 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
letter-spacing: 8px;
line-height: 1;
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
animation: number-pulse 1.5s ease-in-out infinite;
}
.call-clinic {
@@ -749,17 +751,17 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
.clinic-box {
background: var(--color-neutral-100);
border: 2px solid var(--color-success-200);
border: 2px solid var(--color-primary-200);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 146, 98, 0.12);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.12);
min-height: 240px;
display: flex;
flex-direction: column;
}
.clinic-header-bar {
background: linear-gradient(135deg, var(--color-success-600) 0%, var(--color-success-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
padding: 16px 20px;
display: flex;
justify-content: space-between;
@@ -797,7 +799,7 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
flex-direction: column;
justify-content: center;
padding: 20px;
background: var(--color-success-100);
background: var(--color-primary-50);
}
.tipe-layanan-section {
@@ -807,12 +809,12 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
.tipe-label {
font-size: 20px;
font-weight: 700;
color: var(--color-success-700);
color: var(--color-primary-700);
margin-bottom: 12px;
letter-spacing: 1px;
text-transform: uppercase;
padding: 8px 12px;
background: var(--color-success-200);
background: var(--color-primary-200);
border-radius: 8px;
display: inline-block;
}
@@ -837,7 +839,7 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
.current-label {
font-size: 16px;
font-weight: 700;
color: var(--color-success-600);
color: var(--color-primary-600);
margin-bottom: 8px;
letter-spacing: 1px;
text-transform: uppercase;
@@ -874,13 +876,13 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
}
.next-item {
background: var(--color-success-600);
background: var(--color-primary-600);
color: var(--color-neutral-100);
padding: 8px 16px;
border-radius: 12px;
font-size: 22px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(0, 146, 98, 0.25);
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.25);
letter-spacing: 1px;
min-width: 60px;
text-align: center;
@@ -891,13 +893,13 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
/* ========== FOOTER STATS ========== */
.footer-stats-bar {
background: var(--color-neutral-100);
border: 2px solid var(--color-success-200);
border: 2px solid var(--color-primary-200);
border-radius: 16px;
padding: 20px 40px;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 4px 12px rgba(0, 146, 98, 0.12);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.12);
}
.stat-item {
@@ -917,11 +919,11 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
.stat-icon-total,
.stat-icon-waiting {
background: var(--color-success-200);
background: var(--color-primary-200);
}
.stat-icon-active {
background: var(--color-success-200);
background: var(--color-primary-200);
}
.stat-info {
@@ -947,7 +949,7 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
.stat-divider {
width: 2px;
height: 60px;
background: var(--color-success-200);
background: var(--color-primary-200);
}
.footer-message {
@@ -1247,6 +1249,25 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
}
}
/* Animations */
@keyframes pulse-highlight {
0%, 100% {
box-shadow: 0 12px 32px rgba(33, 150, 243, 0.35);
}
50% {
box-shadow: 0 12px 48px rgba(33, 150, 243, 0.55);
}
}
@keyframes number-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
/* Prevent text selection */
* {
user-select: none;
+27 -17
View File
@@ -3,7 +3,11 @@
<div class="klinik-selection-container">
<div class="selection-header">
<div class="header-icon">
<v-icon size="64" color="white">mdi-door-open</v-icon>
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
alt="RSUD Logo"
class="header-logo"
/>
</div>
<div class="header-content">
<h1 class="main-title">Pilih Klinik Ruang</h1>
@@ -19,7 +23,7 @@
@click="navigateToKlinik(klinik.kodeKlinik)"
>
<div class="klinik-card-header">
<v-icon size="32" color="success">mdi-hospital-building</v-icon>
<v-icon size="32" color="primary">mdi-hospital-building</v-icon>
<div class="klinik-info">
<h3 class="klinik-name">{{ klinik.namaKlinik }}</h3>
<p class="klinik-code">{{ klinik.kodeKlinik }}</p>
@@ -28,7 +32,7 @@
<div class="klinik-ruang-preview">
<div class="ruang-count">
<v-icon size="18" color="success">mdi-door</v-icon>
<v-icon size="18" color="primary">mdi-door</v-icon>
<span>{{ klinik.ruangList.length }} Ruang</span>
</div>
<div class="ruang-tags">
@@ -52,7 +56,7 @@
<div class="klinik-card-footer">
<v-btn
color="success"
color="primary"
variant="flat"
size="large"
block
@@ -70,7 +74,7 @@
<h3>Tidak Ada Klinik dengan Ruang Tersedia</h3>
<p>Silakan tambah ruang klinik terlebih dahulu di halaman master</p>
<v-btn
color="success"
color="primary"
variant="flat"
@click="navigateToSettings"
class="mt-4"
@@ -181,24 +185,32 @@ const navigateToSettings = () => {
}
.selection-header {
background: linear-gradient(135deg, var(--color-success-600) 0%, var(--color-success-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(0, 146, 98, 0.3);
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.3);
}
.header-icon {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 24px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 80px;
height: 80px;
object-fit: contain;
padding: 4px;
}
.header-content {
@@ -207,17 +219,15 @@ const navigateToSettings = () => {
}
.main-title {
font-size: 48px;
font-size: 40px;
font-weight: 800;
margin: 0;
letter-spacing: 2px;
color: var(--color-neutral-100);
line-height: 1.2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 20px;
font-size: 18px;
font-weight: 600;
margin: 8px 0 0 0;
color: var(--color-neutral-100);
@@ -246,8 +256,8 @@ const navigateToSettings = () => {
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 146, 98, 0.2);
border-color: var(--color-success-600);
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.2);
border-color: var(--color-primary-600);
}
}
@@ -288,7 +298,7 @@ const navigateToSettings = () => {
gap: 6px;
font-size: 14px;
font-weight: 600;
color: var(--color-success-600);
color: var(--color-primary-600);
margin-bottom: 12px;
}
@@ -299,8 +309,8 @@ const navigateToSettings = () => {
}
.chip-preview {
background-color: var(--color-success-300) !important;
color: var(--color-success-700) !important;
background-color: var(--color-primary-300) !important;
color: var(--color-primary-700) !important;
font-weight: 500;
font-size: 12px;
}
+17 -7
View File
@@ -3,7 +3,11 @@
<div class="loket-selection-container">
<div class="selection-header">
<div class="header-icon">
<v-icon size="64" color="white">mdi-view-dashboard</v-icon>
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
alt="RSUD Logo"
class="header-logo"
/>
</div>
<div class="header-content">
<h1 class="main-title">Pilih Layar Antrian Loket</h1>
@@ -56,7 +60,7 @@
variant="flat"
size="large"
block
class="btn-view"
class="btn-view text-white"
>
<v-icon left>mdi-eye</v-icon>
Tampilkan
@@ -174,13 +178,21 @@ const getKlinikName = (kode) => {
}
.header-icon {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 24px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 80px;
height: 80px;
object-fit: contain;
padding: 4px;
}
.header-content {
@@ -189,17 +201,15 @@ const getKlinikName = (kode) => {
}
.main-title {
font-size: 48px;
font-size: 40px;
font-weight: 800;
margin: 0;
letter-spacing: 2px;
color: var(--color-neutral-100);
line-height: 1.2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 20px;
font-size: 18px;
font-weight: 600;
margin: 8px 0 0 0;
color: var(--color-neutral-100);
+213 -49
View File
@@ -30,28 +30,48 @@
</div>
</div>
<!-- Tabs -->
<v-card-text class="tabs-section">
<v-tabs
v-model="activeTab"
color="primary"
align-tabs="start"
show-arrows
class="status-tabs"
<!-- Status Filter Section -->
<v-card-text class="filter-section-top">
<div class="section-label">DATA PASIEN</div>
<v-chip-group
v-model="activeTab"
mandatory
class="status-filter"
>
<v-tab value="all" @click="filterByStatus('all')">
Semua Pasien <v-badge color="grey-lighten-1" :content="totalCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Menunggu Poli" @click="filterByStatus('Menunggu Poli')">
Menunggu Poli <v-badge color="orange-lighten-1" :content="waitingCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Diperiksa Dokter" @click="filterByStatus('Diperiksa Dokter')">
Diperiksa Dokter <v-badge color="green-lighten-1" :content="examiningCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Selesai Pelayanan" @click="filterByStatus('Selesai Pelayanan')">
Selesai Pelayanan <v-badge color="blue-lighten-1" :content="doneCount" inline class="ml-2"></v-badge>
</v-tab>
</v-tabs>
<v-chip
value="all"
:class="{ 'active-chip': activeTab === 'all' }"
@click="filterByStatus('all')"
>
<v-icon start size="16">mdi-account-group</v-icon>
Semua Pasien ({{ totalCount }})
</v-chip>
<v-chip
value="Menunggu Poli"
:class="{ 'active-chip': activeTab === 'Menunggu Poli' }"
@click="filterByStatus('Menunggu Poli')"
>
<v-icon start size="16">mdi-clock-outline</v-icon>
Menunggu Poli ({{ waitingCount }})
</v-chip>
<v-chip
value="Diperiksa Dokter"
:class="{ 'active-chip': activeTab === 'Diperiksa Dokter' }"
@click="filterByStatus('Diperiksa Dokter')"
>
<v-icon start size="16">mdi-doctor</v-icon>
Diperiksa Dokter ({{ examiningCount }})
</v-chip>
<v-chip
value="Selesai Pelayanan"
:class="{ 'active-chip': activeTab === 'Selesai Pelayanan' }"
@click="filterByStatus('Selesai Pelayanan')"
>
<v-icon start size="16">mdi-check-circle</v-icon>
Selesai Pelayanan ({{ doneCount }})
</v-chip>
</v-chip-group>
</v-card-text>
<!-- Filter Section -->
@@ -62,9 +82,11 @@
<v-text-field
label="No. RM"
v-model="filters.rm_number"
prepend-inner-icon="mdi-card-account-details"
variant="outlined"
density="compact"
hide-details
clearable
class="input-field"
></v-text-field>
</v-col>
@@ -72,18 +94,21 @@
<v-text-field
label="No. Antrean"
v-model="filters.queue_number"
prepend-inner-icon="mdi-numeric"
variant="outlined"
density="compact"
hide-details
type="number"
clearable
class="input-field"
></v-text-field>
</v-col>
<v-col cols="12" sm="2">
<v-col cols="12" sm="3">
<v-select
label="Layanan"
v-model="filters.service"
:items="['Klinik Jiwa', 'Radiologi', 'Fisioterapi', 'Klinik Umum']"
prepend-inner-icon="mdi-hospital-building"
variant="outlined"
density="compact"
hide-details
@@ -93,7 +118,6 @@
</v-col>
<v-col cols="12" sm="2">
<v-btn
color="success"
block
height="40"
@click="applyFilters"
@@ -104,6 +128,18 @@
Cari
</v-btn>
</v-col>
<v-col cols="12" sm="2">
<v-btn
block
height="40"
@click="resetFilters"
class="btn-reset"
variant="outlined"
>
<v-icon left size="18">mdi-refresh</v-icon>
Reset
</v-btn>
</v-col>
</v-row>
</div>
</v-card-text>
@@ -117,11 +153,24 @@
class="elevation-0 data-table"
:search="currentSearchTerm"
>
<template v-slot:item.no="{ index }">
{{ index + 1 }}
</template>
<template v-slot:item.status="{ item }">
<v-chip :color="getStatusColor(item.status)" size="small" class="font-weight-medium status-chip" label>
<v-chip
size="small"
:class="getStatusChipClass(item.status)"
>
<v-icon
:icon="getStatusIcon(item.status)"
size="14"
class="mr-1"
></v-icon>
{{ item.status }}
</v-chip>
</template>
<template v-slot:item.aksi="{ item }">
<v-btn
size="small"
@@ -133,10 +182,12 @@
Lihat
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="info" icon="mdi-information-outline">
Tidak ada data pasien untuk ditampilkan.
</v-alert>
<div class="pa-8 text-center">
<v-icon size="48" color="grey-lighten-1" class="mb-2">mdi-information-outline</v-icon>
<p class="text-body-1 text-grey-darken-1">Tidak ada data pasien untuk ditampilkan.</p>
</div>
</template>
</v-data-table>
</v-card-text>
@@ -179,12 +230,13 @@ const currentSearchTerm = ref('');
// Table Headers
const headers = [
{ title: 'No. RM', key: 'rm_number' },
{ title: 'Nama', key: 'name' },
{ title: 'No. Antrean', key: 'queue_number' },
{ title: 'Layanan', key: 'service' },
{ title: 'Status', key: 'status' },
{ title: 'Aksi', key: 'aksi', sortable: false },
{ title: 'No', key: 'no', sortable: false, width: '60px' },
{ title: 'No. RM', key: 'rm_number', sortable: true },
{ title: 'Nama', key: 'name', sortable: true },
{ title: 'No. Antrean', key: 'queue_number', sortable: true, width: '120px' },
{ title: 'Layanan', key: 'service', sortable: true },
{ title: 'Status', key: 'status', sortable: true, width: '180px' },
{ title: 'Aksi', key: 'aksi', sortable: false, width: '120px' },
];
// Mock Patient Data
@@ -230,6 +282,17 @@ const applyFilters = () => {
activeTab.value = 'all';
};
// Reset all filters
const resetFilters = () => {
filters.value.rm_number = '';
filters.value.qr_code = '';
filters.value.queue_number = '';
filters.value.service = null;
currentSearchTerm.value = '';
filteredPatientList.value = ALL_PATIENTS_DATA;
activeTab.value = 'all';
};
// 2. Logic Filter Status (saat tab ditekan)
const filterByStatus = (status) => {
// Pastikan status tab diperbarui
@@ -277,12 +340,20 @@ const paginatedPatients = computed(() => {
// === UTILITIES & NAVIGATION ===
// Utility function to set color based on status
const getStatusColor = (status) => {
if (status === 'Diperiksa Dokter') return 'green-darken-1';
if (status === 'Menunggu Poli') return 'orange-darken-1';
if (status === 'Selesai Pelayanan') return 'blue-darken-2';
return 'grey';
// Utility function to set chip class based on status
const getStatusChipClass = (status) => {
if (status === 'Diperiksa Dokter') return 'chip-success';
if (status === 'Menunggu Poli') return 'chip-warning';
if (status === 'Selesai Pelayanan') return 'chip-primary';
return 'chip-neutral';
};
// Utility function to set icon based on status
const getStatusIcon = (status) => {
if (status === 'Diperiksa Dokter') return 'mdi-doctor';
if (status === 'Menunggu Poli') return 'mdi-clock-outline';
if (status === 'Selesai Pelayanan') return 'mdi-check-circle';
return 'mdi-help-circle';
};
// Function to navigate to the detailed patient information page
@@ -386,26 +457,53 @@ $font-weight-semibold: 600;
}
// ============================================
// TABS SECTION
// FILTER SECTION TOP
// ============================================
.tabs-section {
padding: 24px 24px 0 24px !important;
.filter-section-top {
padding: 24px 24px 16px 24px !important;
background: $neutral-100;
}
.status-tabs {
font-family: $font-family-base;
.section-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: $neutral-600;
text-transform: uppercase;
margin-bottom: 12px;
}
.v-tab.v-tab--selected {
.status-filter {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.status-filter .v-chip {
font-size: 12px;
font-weight: $font-weight-semibold;
height: 32px;
border: 1px solid $neutral-500;
background: $neutral-100;
color: $neutral-600;
transition: all 0.2s ease;
&:hover {
background: $neutral-300;
}
}
.status-filter .v-chip.active-chip {
background: $primary-600;
color: $neutral-100;
border-color: $primary-600;
}
// ============================================
// FILTER SECTION
// ============================================
.filter-section {
padding: 24px !important;
padding: 0 24px 24px 24px !important;
background: $neutral-100;
}
@@ -422,12 +520,29 @@ $font-weight-semibold: 600;
}
.btn-search {
background-color: $success-600 !important;
background-color: $primary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
&:hover {
background-color: $primary-700 !important;
}
}
.btn-reset {
border-color: $neutral-600 !important;
color: $neutral-800 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
&:hover {
background-color: $neutral-300 !important;
}
}
// ============================================
@@ -435,21 +550,70 @@ $font-weight-semibold: 600;
// ============================================
.data-table {
font-family: $font-family-base;
:deep(.v-data-table__th) {
font-weight: $font-weight-semibold;
color: $neutral-800;
font-size: 14px;
line-height: 20px;
}
:deep(.v-data-table__td) {
font-size: 14px;
line-height: 20px;
color: $neutral-900;
}
}
.status-chip {
// ============================================
// CHIPS
// ============================================
.chip-success {
background-color: $success-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-warning {
background-color: #F59E0B !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-primary {
background-color: $primary-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-neutral {
background-color: $neutral-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
// ============================================
// BUTTONS
// ============================================
.btn-view {
background-color: $success-600 !important;
background-color: $primary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
&:hover {
background-color: $primary-700 !important;
}
}
// ============================================
-6
View File
@@ -1,11 +1,5 @@
<template>
<div class="hak-akses-page pa-4">
<v-breadcrumbs :items="breadcrumbs" class="pl-0 mb-4">
<template v-slot:divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<!-- Dialog for Add/Edit Form -->
<v-dialog
v-model="showAddEditDialog"
-15
View File
@@ -1,20 +1,5 @@
<template>
<div class="user-login-page pa-4">
<!-- Display Authentication Info for context -->
<div class="mb-4 pa-3 bg-blue-grey-lighten-5 rounded-lg text-caption">
User Status:
<v-chip :color="currentUserData ? 'green' : 'red'" size="small" class="ml-2 mr-1">
{{ currentUserData ? 'Authenticated' : 'Unauthenticated' }}
</v-chip>
(Username: <strong>{{ currentUserData?.namaUser || currentUserData?.namaLengkap || 'N/A' }}</strong>)
</div>
<v-breadcrumbs :items="breadcrumbs" class="pl-0">
<template #divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<v-alert
v-if="fetchError"
type="error"
+220 -23
View File
@@ -31,22 +31,96 @@
</div>
<!-- Search & Content -->
<v-card-text class="pa-0">
<!-- Search & Tabs -->
<VerificationSearchTabs
v-model:search-query="search"
v-model:selected-tab="filterStatus"
:all-count="patients.length"
:pending-count="unverifiedPatients.length"
:verified-count="verifiedPatients.length"
/>
<v-card-text>
<!-- Search & Filter -->
<div class="search-filter-section">
<div class="section-label">DATA PASIEN</div>
<!-- Status Filter Chips -->
<v-chip-group
v-model="filterStatus"
mandatory
class="status-filter mb-3"
>
<v-chip
:value="0"
:class="{ 'active-chip': filterStatus === 0 }"
>
<v-icon start size="16">mdi-account-group</v-icon>
Semua ({{ patients.length }})
</v-chip>
<v-chip
:value="1"
:class="{ 'active-chip': filterStatus === 1 }"
>
<v-icon start size="16">mdi-clock-alert</v-icon>
Belum Verifikasi ({{ unverifiedPatients.length }})
</v-chip>
<v-chip
:value="2"
:class="{ 'active-chip': filterStatus === 2 }"
>
<v-icon start size="16">mdi-check-decagram</v-icon>
Terverifikasi ({{ verifiedPatients.length }})
</v-chip>
</v-chip-group>
<!-- Patient List -->
<PatientVerificationList
:patients="filteredPatients"
:empty-message="noDataText"
@verify="openDaftarModal"
/>
<!-- Search Field -->
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
placeholder="Cari nama atau No. RM..."
variant="outlined"
density="compact"
hide-details
clearable
class="search-field"
></v-text-field>
</div>
<!-- Data Table -->
<v-data-table
:headers="headers"
:items="filteredPatients"
:items-per-page="10"
class="elevation-0 data-table"
>
<template v-slot:item.no="{ index }">
{{ index + 1 }}
</template>
<template v-slot:item.status="{ item }">
<v-chip
size="small"
:class="item.status === 'Terverifikasi' ? 'chip-success' : 'chip-warning'"
>
<v-icon
:icon="item.status === 'Terverifikasi' ? 'mdi-check-circle' : 'mdi-clock-alert'"
size="14"
class="mr-1"
></v-icon>
{{ item.status }}
</v-chip>
</template>
<template v-slot:item.telepon="{ item }">
<span v-if="item.telepon">{{ item.telepon }}</span>
<v-chip v-else size="small" class="chip-neutral">Belum diisi</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
v-if="item.status === 'Belum Terverifikasi'"
size="small"
@click="openDaftarModal(item)"
variant="flat"
class="btn-verify"
>
<v-icon size="16" left>mdi-qrcode-scan</v-icon>
Verifikasi
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
@@ -80,8 +154,6 @@
import { ref, computed } from 'vue';
import { useDisplay } from 'vuetify';
import QrcodeVue from 'qrcode.vue';
import VerificationSearchTabs from '@/components/verification/VerificationSearchTabs.vue';
import PatientVerificationList from '@/components/verification/PatientVerificationList.vue';
import QRVerificationDialog from '@/components/verification/QRVerificationDialog.vue';
definePageMeta({
@@ -90,6 +162,17 @@ definePageMeta({
const display = useDisplay();
// Table Headers
const headers = ref([
{ title: 'No', value: 'no', sortable: false, width: '60px' },
{ title: 'Nama Pasien', value: 'nama', sortable: true },
{ title: 'No. RM', value: 'rm', sortable: true },
{ title: 'Alamat', value: 'alamat', sortable: false },
{ title: 'No. Telepon', value: 'telepon', sortable: false },
{ title: 'Status', value: 'status', sortable: true, width: '180px' },
{ title: 'Actions', value: 'actions', sortable: false, width: '200px' },
]);
// Data
const patients = ref([
{ nama: 'Budi Santoso', rm: '00123456', alamat: 'Jl. Melati No. 10', telepon: '', status: 'Belum Terverifikasi' },
@@ -114,12 +197,6 @@ const currentDate = computed(() => {
return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
});
const noDataText = computed(() =>
search.value
? 'Tidak ada data pasien yang cocok dengan pencarian.'
: 'Tidak ada pasien dengan status ini.'
);
const unverifiedPatients = computed(() =>
patients.value.filter((p) => p.status === 'Belum Terverifikasi')
);
@@ -309,6 +386,126 @@ $font-weight-semibold: 600;
display: block;
}
// ============================================
// SEARCH & FILTER SECTION
// ============================================
.search-filter-section {
margin-bottom: 24px;
}
.section-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: $neutral-600;
text-transform: uppercase;
margin-bottom: 12px;
}
.status-filter {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.status-filter .v-chip {
font-size: 12px;
font-weight: $font-weight-semibold;
height: 32px;
border: 1px solid $neutral-500;
background: $neutral-100;
color: $neutral-600;
transition: all 0.2s ease;
&:hover {
background: $neutral-300;
}
}
.status-filter .v-chip.active-chip {
background: $primary-600;
color: $neutral-100;
border-color: $primary-600;
}
.search-field {
max-width: 400px;
font-size: 14px;
line-height: 20px;
}
// ============================================
// DATA TABLE
// ============================================
.data-table {
font-family: $font-family-base;
:deep(.v-data-table__th) {
font-weight: $font-weight-semibold;
color: $neutral-800;
font-size: 14px;
line-height: 20px;
}
:deep(.v-data-table__td) {
font-size: 14px;
line-height: 20px;
color: $neutral-900;
}
}
// ============================================
// CHIPS
// ============================================
.chip-success {
background-color: #009262 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-success-outline {
border: 1px solid #009262;
background-color: transparent !important;
color: #009262 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-warning {
background-color: #F59E0B !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.chip-neutral {
background-color: $neutral-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
// ============================================
// BUTTONS
// ============================================
.btn-verify {
background-color: $primary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
&:hover {
background-color: $primary-700 !important;
}
}
// ============================================
// RESPONSIVE
// ============================================