update tampilan dan flow di loket dan master

This commit is contained in:
bagus-arie05
2026-01-05 09:26:14 +07:00
parent b5e40c68d2
commit 0a2453bdab
7 changed files with 849 additions and 399 deletions
@@ -11,7 +11,7 @@
</v-card-title>
<v-card-text class="pa-4">
<!-- Patient Info Card -->
<!-- patient info card -->
<div v-if="patient" class="patient-card mb-4">
<h4 class="card-title">Informasi Pasien</h4>
<v-row>
@@ -26,7 +26,7 @@
</v-row>
</div>
<!-- Search Field -->
<!-- search field -->
<v-text-field
v-model="searchModel"
prepend-inner-icon="mdi-magnify"
@@ -37,7 +37,7 @@
class="mb-4 search-field"
/>
<!-- Options Grid -->
<!-- options grid -->
<v-row>
<v-col
v-for="option in filteredOptions"
+1 -1
View File
@@ -55,7 +55,7 @@
</div>
</div>
<!-- Action Button for non-clickable states -->
<div v-if="!isClickable && patient.status === 'terlambat'" class="card-actions mt-3">
<v-btn
block
+70 -34
View File
@@ -14,6 +14,7 @@
:value="status.value"
:class="{ 'active-chip': selectedStatusModel === status.value }"
>
<v-icon v-if="status.icon" start size="16">{{ status.icon }}</v-icon>
{{ status.label }} ({{ status.count }})
</v-chip>
</v-chip-group>
@@ -76,21 +77,6 @@
</template>
</v-select>
<v-select
v-model="selectedFastTrack"
:items="fastTrackOptions"
label="Filter Fast Track"
density="compact"
hide-details
clearable
class="filter-select"
variant="outlined"
>
<template #prepend-inner>
<v-icon size="20">mdi-flash</v-icon>
</template>
</v-select>
<v-btn
v-if="hasActiveFilters"
variant="text"
@@ -130,12 +116,12 @@
{{ selectedShift }}
</v-chip>
<v-chip
v-if="selectedFastTrack"
v-if="selectedFastTrackModel"
size="small"
closable
@click:close="selectedFastTrack = null"
@click:close="selectedFastTrackModel = null"
>
Fast Track: {{ selectedFastTrack }}
Fast Track: {{ selectedFastTrackModel }}
</v-chip>
</div>
</div>
@@ -231,21 +217,55 @@ const props = defineProps({
terlambat: 'Terlambat',
pending: 'Pending'
})
},
selectedFastTrack: {
type: String,
default: null
},
fastTrackOptions: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:selectedStatus', 'update:searchQuery', 'action']);
const emit = defineEmits(['update:selectedStatus', 'update:searchQuery', 'update:selectedFastTrack', 'action']);
const currentPage = ref(1);
const selectedKlinik = ref(null);
const selectedPembayaran = ref(null);
const selectedShift = ref(null);
const selectedFastTrack = ref(null);
const selectedStatusModel = computed({
get: () => props.selectedStatus,
const selectedFastTrackModel = computed({
get: () => props.selectedFastTrack,
set: (value) => {
currentPage.value = 1;
emit('update:selectedFastTrack', value);
}
});
const selectedStatusModel = computed({
get: () => {
// If Fast Track is selected, return 'fasttrack' as status
if (selectedFastTrackModel.value === 'YA') {
return 'fasttrack';
}
return props.selectedStatus;
},
set: (value) => {
currentPage.value = 1;
// Handle Fast Track selection
if (value === 'fasttrack') {
selectedFastTrackModel.value = 'YA';
// Emit 'all' as status since Fast Track is a separate filter
emit('update:selectedStatus', 'all');
return;
}
// Reset Fast Track when other status is selected
if (selectedFastTrackModel.value) {
selectedFastTrackModel.value = null;
}
emit('update:selectedStatus', value);
}
});
@@ -280,6 +300,16 @@ const statusOptions = computed(() => {
{ value: 'pending', label: props.statusLabels.pending, count: props.pendingCount }
);
// Add Fast Track as a status option
if (fastTrackYaCount.value > 0) {
baseOptions.push({
value: 'fasttrack',
label: 'Fast Track',
count: fastTrackYaCount.value,
icon: 'mdi-flash'
});
}
return baseOptions;
});
@@ -299,28 +329,36 @@ const shiftOptions = computed(() => {
return shifts.sort();
});
const fastTrackOptions = computed(() => {
const normalizedValues = props.items
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
.filter((v) => v.length > 0);
const uniqueTracks = [...new Set(normalizedValues)];
return uniqueTracks.sort();
// Count Fast Track "YA"
const fastTrackYaCount = computed(() => {
return props.items.filter(p => {
const patientFastTrack = (p.fastTrack ?? "").toString().trim().toUpperCase();
return patientFastTrack === 'YA';
}).length;
});
const hasActiveFilters = computed(() => {
return !!(selectedKlinik.value || selectedPembayaran.value || selectedShift.value || selectedFastTrack.value);
return !!(selectedKlinik.value || selectedPembayaran.value || selectedShift.value || selectedFastTrackModel.value);
});
const clearAllFilters = () => {
selectedKlinik.value = null;
selectedPembayaran.value = null;
selectedShift.value = null;
selectedFastTrack.value = null;
selectedFastTrackModel.value = null;
currentPage.value = 1;
};
const filteredItems = computed(() => {
// Handle Fast Track as a status category
if (selectedStatusModel.value === 'fasttrack') {
return props.items.filter(p => {
const patientFastTrack = (p.fastTrack ?? "").toString().trim().toUpperCase();
return patientFastTrack === 'YA';
});
}
if (selectedStatusModel.value === 'all') return props.items;
return props.items.filter(p => p.status === selectedStatusModel.value);
});
@@ -338,9 +376,7 @@ const filteredAndSearchedItems = computed(() => {
if (selectedShift.value) {
result = result.filter(p => p.shift === selectedShift.value);
}
if (selectedFastTrack.value) {
result = result.filter(p => p.fastTrack === selectedFastTrack.value);
}
// Fast Track filtering is now handled in filteredItems as a status category
// Apply search
if (searchModel.value) {
+214 -57
View File
@@ -13,71 +13,127 @@
<v-row class="content-grid" dense>
<!-- Left Column: Current Patient & Queue Actions -->
<v-col cols="12" md="5">
<!-- Current Patient Card -->
<CurrentPatientCard
:patient="currentProcessingPatient"
theme="secondary"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<div class="sticky-wrapper">
<!-- Current Patient Card -->
<CurrentPatientCard
:patient="currentProcessingPatient"
theme="secondary"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="150"
:used-quota="quotaUsed"
:has-next="!!nextPatient"
@call="handleCall"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="150"
:used-quota="quotaUsed"
:has-next="!!nextPatient"
@call="handleCall"
/>
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
</div>
</div>
</v-col>
<!-- Right Column: Patient Table -->
<v-col cols="12" md="7">
<PatientDataTable
:items="allPatients"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
:di-loket-count="(diLoketPatients || []).length"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:status-labels="statusLabels"
:show-diproses="false"
@action="handleTableAction"
/>
<v-card class="patient-data-container" elevation="0">
<v-card-text class="pa-4">
<!-- Header with Filters -->
<div class="data-header mb-4">
<div class="section-label">DATA PASIEN</div>
<div class="filters">
<!-- Status Filter -->
<v-chip-group v-model="selectedStatus" mandatory class="status-filter">
<v-chip value="all" :class="{ 'active-chip': selectedStatus === 'all' }">
Semua ({{ allPatients.length }})
</v-chip>
<v-chip value="diloket" :class="{ 'active-chip': selectedStatus === 'diloket' }">
Di Klinik ({{ (diLoketPatients || []).length }})
</v-chip>
<v-chip value="terlambat" :class="{ 'active-chip': selectedStatus === 'terlambat' }">
Terlambat ({{ (terlambatPatients || []).length }})
</v-chip>
<v-chip value="pending" :class="{ 'active-chip': selectedStatus === 'pending' }">
Pending ({{ (pendingPatients || []).length }})
</v-chip>
</v-chip-group>
<!-- Fast Track Filter -->
<v-select
v-model="selectedFastTrack"
:items="fastTrackOptions"
label="Filter Fast Track"
density="compact"
hide-details
clearable
class="fast-track-filter"
variant="outlined"
>
<template #prepend-inner>
<v-icon size="20">mdi-flash</v-icon>
</template>
</v-select>
<!-- Search Field -->
<v-text-field
v-model="searchQuery"
placeholder="Cari barcode, nomor antrian..."
density="compact"
hide-details
class="search-field"
prepend-inner-icon="mdi-magnify"
/>
</div>
</div>
<PatientDataTable
:items="allPatients"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
v-model:selected-fast-track="selectedFastTrack"
:di-loket-count="(diLoketPatients || []).length"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:status-labels="statusLabels"
:show-diproses="false"
@action="handleTableAction"
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
@@ -170,6 +226,16 @@ const currentDate = ref(
const selectedStatus = ref("all");
const searchQuery = ref("");
const selectedFastTrack = ref(null);
// Fast Track options from all patients
const fastTrackOptions = computed(() => {
const normalizedValues = allPatients.value
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
.filter((v) => v.length > 0);
const uniqueTracks = [...new Set(normalizedValues)];
return uniqueTracks.sort();
});
// Custom status labels for Klinik
const statusLabels = {
@@ -254,9 +320,100 @@ const handleProcessNext = () => {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.sticky-wrapper {
position: sticky;
top: 16px;
align-self: flex-start;
max-height: calc(100vh - 32px);
overflow-y: auto;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.status-filter {
flex: 1;
min-width: 300px;
}
.status-filter .v-chip {
font-size: 12px;
font-weight: 600;
height: 32px;
border: 1px solid var(--color-neutral-500);
background: var(--color-neutral-100);
color: var(--color-neutral-600);
transition: all 0.2s ease;
}
.status-filter .v-chip:hover {
background: var(--color-neutral-300);
}
.status-filter .v-chip.active-chip {
background: var(--color-secondary-600);
color: var(--color-neutral-100);
border-color: var(--color-secondary-600);
}
.fast-track-filter {
min-width: 180px;
max-width: 220px;
}
.search-field {
max-width: 300px;
min-width: 250px;
}
.section-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: var(--color-neutral-600);
text-transform: uppercase;
}
.data-header {
display: flex;
flex-direction: column;
gap: 12px;
}
.patient-data-container {
border-radius: 12px;
border: 1px solid var(--color-neutral-500);
background: var(--color-neutral-100);
}
@media (max-width: 960px) {
.loket-container {
padding: 12px;
}
.sticky-wrapper {
position: relative;
top: 0;
max-height: none;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.status-filter {
min-width: 100%;
}
.search-field,
.fast-track-filter {
max-width: 100%;
min-width: 100%;
}
}
</style>
+176 -57
View File
@@ -13,71 +13,95 @@
<v-row class="content-grid" dense>
<!-- Left Column: Current Patient & Queue Actions -->
<v-col cols="12" md="5">
<!-- Current Patient Card -->
<CurrentPatientCard
:patient="currentProcessingPatient"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<div class="sticky-wrapper">
<!-- Current Patient Card -->
<CurrentPatientCard
:patient="currentProcessingPatient"
:has-next-queue="(diLoketPatients || []).length > 0"
:next-queue-info="nextQueueInfo"
@action="handlePatientAction"
@change-klinik="showChangeKlinikDialog = true"
@process-next="handleProcessNext"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="150"
:used-quota="quotaUsed"
:has-next="!!nextPatient"
@call="handleCall"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="150"
:used-quota="quotaUsed"
:has-next="!!nextPatient"
@call="handleCall"
/>
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<!-- Create Queue Buttons -->
<div class="create-buttons mt-3">
<v-row no-gutters>
<v-col cols="6" class="pr-2">
<v-btn
block
class="py-6"
color="primary-600"
:disabled="!currentProcessingPatient"
@click="showKlinikDialog = true"
>
<v-icon start>mdi-hospital-building</v-icon>
Buat Antrean Klinik
</v-btn>
</v-col>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
<v-col cols="6" class="pl-2">
<v-btn
block
class="py-6 text-white"
color="secondary-600"
:disabled="!currentProcessingPatient"
@click="openPenunjangDialog()"
>
<v-icon start>mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</v-col>
</v-row>
</div>
</div>
</v-col>
<!-- Right Column: Patient Table -->
<v-col cols="12" md="7">
<PatientDataTable
:items="allPatientsForStage"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
:di-loket-count="diLoketCount"
:diproses-count="currentProcessingPatient ? 1 : 0"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:show-diproses="false"
@action="handleTableAction"
/>
<v-card class="patient-data-container" elevation="0">
<v-card-text class="pa-4">
<!-- Header with Filters -->
<div class="data-header mb-4">
<div class="section-label">DATA PASIEN</div>
<div class="filters">
<!-- Search Field -->
<v-text-field
v-model="searchQuery"
placeholder="Cari barcode, nomor antrian..."
density="compact"
hide-details
class="search-field"
prepend-inner-icon="mdi-magnify"
/>
</div>
</div>
<PatientDataTable
:items="allPatientsForStage"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
v-model:selected-fast-track="selectedFastTrack"
:di-loket-count="diLoketCount"
:diproses-count="currentProcessingPatient ? 1 : 0"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:show-diproses="false"
:fast-track-options="fastTrackOptions"
@action="handleTableAction"
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
@@ -170,6 +194,16 @@ const currentDate = ref(
const selectedStatus = ref("all");
const searchQuery = ref("");
const selectedFastTrack = ref(null);
// Fast Track options from all patients
const fastTrackOptions = computed(() => {
const normalizedValues = allPatientsForStage.value
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
.filter((v) => v.length > 0);
const uniqueTracks = [...new Set(normalizedValues)];
return uniqueTracks.sort();
});
// Combine all patients with status - PRESERVE ALL PROPERTIES
const allPatientsForStage = computed(() => {
@@ -266,9 +300,94 @@ const handleProcessNext = () => {
font-weight: 600;
}
.sticky-wrapper {
position: sticky;
top: 16px;
align-self: flex-start;
max-height: calc(100vh - 32px);
overflow-y: auto;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.status-filter {
flex: 1;
min-width: 300px;
}
.status-filter .v-chip {
font-size: 12px;
font-weight: 600;
height: 32px;
border: 1px solid var(--color-neutral-500);
background: var(--color-neutral-100);
color: var(--color-neutral-600);
transition: all 0.2s ease;
}
.status-filter .v-chip:hover {
background: var(--color-neutral-300);
}
.status-filter .v-chip.active-chip {
background: var(--color-primary-600);
color: var(--color-neutral-100);
border-color: var(--color-primary-600);
}
.search-field {
max-width: 300px;
min-width: 250px;
}
.section-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: var(--color-neutral-600);
text-transform: uppercase;
}
.data-header {
display: flex;
flex-direction: column;
gap: 12px;
}
.patient-data-container {
border-radius: 12px;
border: 1px solid var(--color-neutral-500);
background: var(--color-neutral-100);
}
@media (max-width: 960px) {
.loket-container {
padding: 12px;
}
.sticky-wrapper {
position: relative;
top: 0;
max-height: none;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.status-filter {
min-width: 100%;
}
.search-field {
max-width: 100%;
min-width: 100%;
}
}
</style>
+82 -66
View File
@@ -18,79 +18,81 @@
<v-row class="content-grid" dense>
<!-- Left Column: Current Patient -->
<v-col cols="12" md="5">
<v-card class="current-patient-card" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">SEDANG DIPROSES</div>
<div v-if="currentProcessingPatient" class="patient-details">
<div class="patient-number mb-2">{{ currentProcessingPatient.noAntrian.split(" |")[0] }}</div>
<div class="patient-info-text mb-3">
<div>{{ currentProcessingPatient.barcode }}</div>
<div>{{ currentProcessingPatient.klinik }} | {{ currentProcessingPatient.pembayaran }}</div>
<div class="sticky-wrapper">
<v-card class="current-patient-card" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">SEDANG DIPROSES</div>
<div v-if="currentProcessingPatient" class="patient-details">
<div class="patient-number mb-2">{{ currentProcessingPatient.noAntrian.split(" |")[0] }}</div>
<div class="patient-info-text mb-3">
<div>{{ currentProcessingPatient.barcode }}</div>
<div>{{ currentProcessingPatient.klinik }} | {{ currentProcessingPatient.pembayaran }}</div>
</div>
<div class="action-grid">
<v-btn color="#10b981" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'check-in')">
<v-icon start size="20">mdi-check</v-icon>
Selesai
</v-btn>
<v-btn color="#f59e0b" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'terlambat')">
<v-icon start size="20">mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn color="#ef4444" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'pending')">
<v-icon start size="20">mdi-pause</v-icon>
Pending
</v-btn>
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showChangeKlinikDialog = true">
<v-icon start size="20">mdi-swap-horizontal</v-icon>
Ubah Ruang
</v-btn>
</div>
</div>
<div class="action-grid">
<v-btn color="#10b981" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'check-in')">
<v-icon start size="20">mdi-check</v-icon>
Selesai
</v-btn>
<v-btn color="#f59e0b" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'terlambat')">
<v-icon start size="20">mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn color="#ef4444" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'pending')">
<v-icon start size="20">mdi-pause</v-icon>
Pending
</v-btn>
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showChangeKlinikDialog = true">
<v-icon start size="20">mdi-swap-horizontal</v-icon>
Ubah Ruang
</v-btn>
<div v-else class="empty-state">
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
<div class="empty-text">Tidak ada pasien yang diproses</div>
</div>
</div>
<div v-else class="empty-state">
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
<div class="empty-text">Tidak ada pasien yang diproses</div>
</div>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
<!-- Queue Actions -->
<v-card class="queue-actions-card mt-3" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">PANGGIL ANTREAN</div>
<div class="quota-info mb-3">
<div class="quota-item">
<span class="quota-label">Kuota</span>
<span class="quota-value">150</span>
<!-- Queue Actions -->
<v-card class="queue-actions-card mt-3" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">PANGGIL ANTREAN</div>
<div class="quota-info mb-3">
<div class="quota-item">
<span class="quota-label">Kuota</span>
<span class="quota-value">150</span>
</div>
<div class="quota-item">
<span class="quota-label">Tersedia</span>
<span class="quota-value quota-available">{{ 150 - quotaUsed }}</span>
</div>
<div class="quota-item full-width">
<v-progress-linear :model-value="(quotaUsed / 150) * 100" color="#10b981" height="6" rounded class="mt-1"></v-progress-linear>
<span class="quota-used">Terpakai: {{ quotaUsed }}</span>
</div>
</div>
<div class="quota-item">
<span class="quota-label">Tersedia</span>
<span class="quota-value quota-available">{{ 150 - quotaUsed }}</span>
</div>
<div class="quota-item full-width">
<v-progress-linear :model-value="(quotaUsed / 150) * 100" color="#10b981" height="6" rounded class="mt-1"></v-progress-linear>
<span class="quota-used">Terpakai: {{ quotaUsed }}</span>
</div>
</div>
<div class="call-buttons">
<v-btn color="#10b981" variant="outlined" @click="callNext" :disabled="!nextPatient">1</v-btn>
<v-btn color="#3b82f6" variant="outlined" @click="callMultiplePatients(5)">5</v-btn>
<v-btn color="#f59e0b" variant="outlined" @click="callMultiplePatients(10)">10</v-btn>
<v-btn color="#ef4444" variant="outlined" @click="callMultiplePatients(20)">20</v-btn>
</div>
</v-card-text>
</v-card>
<div class="call-buttons">
<v-btn color="#10b981" variant="outlined" @click="callNext" :disabled="!nextPatient">1</v-btn>
<v-btn color="#3b82f6" variant="outlined" @click="callMultiplePatients(5)">5</v-btn>
<v-btn color="#f59e0b" variant="outlined" @click="callMultiplePatients(10)">10</v-btn>
<v-btn color="#ef4444" variant="outlined" @click="callMultiplePatients(20)">20</v-btn>
</div>
</v-card-text>
</v-card>
<!-- Create Queue Button -->
<div class="create-buttons mt-3">
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showPenunjangDialog = true">
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
<!-- Create Queue Button -->
<div class="create-buttons mt-3">
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showPenunjangDialog = true">
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</div>
</div>
</v-col>
@@ -424,6 +426,14 @@ const getStatusLabel = (status) => {
padding: 8px;
}
.sticky-wrapper {
position: sticky;
top: 16px;
align-self: flex-start;
max-height: calc(100vh - 32px);
overflow-y: auto;
}
.current-patient-card,
.queue-actions-card,
.patient-table-card {
@@ -663,6 +673,12 @@ const getStatusLabel = (status) => {
padding: 12px;
}
.sticky-wrapper {
position: relative;
top: 0;
max-height: none;
}
.filters {
flex-direction: column;
align-items: stretch;
+303 -181
View File
@@ -8,18 +8,24 @@
</div>
<div class="header-text">
<h1 class="page-title">{{ anjunganData.namaAnjungan }}</h1>
<p class="page-subtitle">Pilih Klinik untuk Pendaftaran - {{ anjunganData.jenisPasien }}</p>
<p class="page-subtitle">
Pilih Klinik untuk Pendaftaran - {{ anjunganData.jenisPasien }}
</p>
</div>
</div>
<div class="header-right">
<div class="info-guide">
<div class="guide-item">
<span class="guide-number">1</span>
<span class="guide-text">Pilih Poli (Hijau = Buka, Merah = Tutup)</span>
<span class="guide-text"
>Pilih Poli (Hijau = Buka, Merah = Tutup)</span
>
</div>
<div class="guide-item">
<span class="guide-number">2</span>
<span class="guide-text">Pilih: Daftar Sekarang atau Jadwal Lain</span>
<span class="guide-text"
>Pilih: Daftar Sekarang atau Jadwal Lain</span
>
</div>
<div class="guide-item">
<span class="guide-number">3</span>
@@ -58,24 +64,42 @@
</h3>
<div class="doctor-info">
<v-icon size="16" :color="clinic.available ? 'var(--color-primary-600)' : 'var(--color-neutral-600)'">
<v-icon
size="16"
:color="
clinic.available
? 'var(--color-primary-600)'
: 'var(--color-neutral-600)'
"
>
mdi-doctor
</v-icon>
<span>{{ getDisplayDoctorInfo(clinic) }}</span>
</div>
<div class="schedule-info">
<v-icon size="14" :color="clinic.available ? 'var(--color-success-600)' : 'var(--color-neutral-600)'">
<v-icon
size="14"
:color="
clinic.available
? 'var(--color-success-600)'
: 'var(--color-neutral-600)'
"
>
mdi-clock-outline
</v-icon>
<span>{{ clinic.schedule || 'Tidak tersedia' }}</span>
<span>{{ clinic.schedule || "Tidak tersedia" }}</span>
</div>
<div class="clinic-icon-wrapper">
<v-icon
:icon="clinic.icon"
size="40"
:color="clinic.available ? 'var(--color-success-600)' : 'var(--color-neutral-100)'"
:color="
clinic.available
? 'var(--color-success-600)'
: 'var(--color-neutral-100)'
"
></v-icon>
</div>
</v-card-text>
@@ -84,14 +108,22 @@
</v-row>
<div v-if="filteredClinics.length === 0" class="empty-state">
<v-icon size="64" color="grey-lighten-1">mdi-hospital-marker-outline</v-icon>
<v-icon size="64" color="grey-lighten-1"
>mdi-hospital-marker-outline</v-icon
>
<h3 class="empty-title">Tidak ada klinik yang sesuai</h3>
<p class="empty-subtitle">Anjungan ini belum memiliki klinik terpilih</p>
<p class="empty-subtitle">
Anjungan ini belum memiliki klinik terpilih
</p>
</div>
</v-card-text>
</v-card>
<v-dialog v-model="showVisitTypeDialog" max-width="400" @click:outside="showVisitTypeDialog = false">
<v-dialog
v-model="showVisitTypeDialog"
max-width="400"
@click:outside="showVisitTypeDialog = false"
>
<v-card class="dialog-card">
<v-card-title class="dialog-header">
<v-icon class="mr-2">mdi-check-circle</v-icon>
@@ -101,7 +133,11 @@
<v-card-text class="pa-6" v-if="selectedClinic">
<div class="dialog-content">
<div class="dialog-icon mb-3">
<v-icon :icon="selectedClinic.icon" size="48" color="primary-600"></v-icon>
<v-icon
:icon="selectedClinic.icon"
size="48"
color="primary-600"
></v-icon>
</div>
<h3 class="dialog-clinic-name">{{ selectedClinic.name }}</h3>
<p v-if="selectedClinic.subtitle" class="dialog-subtitle">
@@ -119,44 +155,56 @@
</li>
</ul>
</div>
<div class="quota-section">
<p><strong>Kuota Hari Ini (Shift {{ getCurrentShiftNumber() }}):</strong></p>
<div class="current-quota-display" :class="{ 'quota-empty': getCurrentShiftQuota() === 0 }">
<span class="quota-large-number">{{ getCurrentShiftQuota() }}</span>
<p>
<strong
>Kuota Hari Ini (Shift
{{ getCurrentShiftNumber() }}):</strong
>
</p>
<div
class="current-quota-display"
:class="{ 'quota-empty': getCurrentShiftQuota() === 0 }"
>
<span class="quota-large-number">{{
getCurrentShiftQuota()
}}</span>
<span class="quota-label">pasien tersedia</span>
</div>
</div>
<p><strong>Jadwal:</strong> {{ selectedClinic.schedule }}</p>
</div>
</div>
</v-card-text>
<v-divider />
<v-card-text class="pa-4">
<v-row dense>
<v-col cols="6">
<v-btn
color="success-600"
class="text-white"
size="large"
block
<v-btn
color="success-600"
class="text-white"
size="large"
block
@click="selectVisitType('SEKARANG')"
:disabled="isShift1Full(selectedClinic)"
:class="{ 'btn-disabled': isShift1Full(selectedClinic) }"
>
DAFTAR SEKARANG
</v-btn>
<p v-if="isShift1Full(selectedClinic)" class="disabled-hint">Kuota hari ini habis</p>
<p v-if="isShift1Full(selectedClinic)" class="disabled-hint">
Kuota hari ini habis
</p>
</v-col>
<v-col cols="6">
<v-btn
color="primary-600"
class="text-white"
size="large"
block
<v-btn
color="primary-600"
class="text-white"
size="large"
block
@click="selectVisitType('JADWAL_LAIN')"
>
JADWAL LAIN
@@ -164,36 +212,66 @@
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="justify-center pa-4 pt-0">
<v-btn variant="outlined" color="neutral-600" @click="showVisitTypeDialog = false">
<v-btn
variant="outlined"
color="neutral-600"
@click="showVisitTypeDialog = false"
>
Tutup
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showPaymentTypeDialog" max-width="400" @click:outside="showPaymentTypeDialog = false">
<v-dialog
v-model="showPaymentTypeDialog"
max-width="400"
@click:outside="showPaymentTypeDialog = false"
>
<v-card class="dialog-card">
<v-card-title class="dialog-header-simple">Jenis Pembayaran</v-card-title>
<v-card-title class="dialog-header-simple"
>Jenis Pembayaran</v-card-title
>
<v-divider />
<v-card-text class="pa-4">
<v-btn color="success-600" class="mb-3 payment-btn text-white" size="x-large" block @click="selectPaymentType('BPJS')">
<v-btn
color="success-600"
class="mb-3 payment-btn text-white"
size="x-large"
block
@click="selectPaymentType('BPJS')"
>
BPJS
</v-btn>
<v-btn color="secondary-600" class="payment-btn text-white" size="x-large" block @click="selectPaymentType('UMUM')">
<v-btn
color="secondary-600"
class="payment-btn text-white"
size="x-large"
block
@click="selectPaymentType('UMUM')"
>
UMUM / JKMM / SPM / DLL
</v-btn>
</v-card-text>
<v-card-actions class="justify-center pa-4 pt-0">
<v-btn variant="outlined" color="neutral-600" @click="showPaymentTypeDialog = false">
<v-btn
variant="outlined"
color="neutral-600"
@click="showPaymentTypeDialog = false"
>
Tutup
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showBookingFormDialog" max-width="500" @click:outside="showBookingFormDialog = false">
<v-dialog
v-model="showBookingFormDialog"
max-width="500"
@click:outside="showBookingFormDialog = false"
>
<v-card class="dialog-card">
<v-card-title class="dialog-header">
Pilih Jadwal Kunjungan
@@ -236,17 +314,31 @@
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn @click="showBookingFormDialog = false" variant="outlined" color="neutral-600">
<v-btn
@click="showBookingFormDialog = false"
variant="outlined"
color="neutral-600"
>
Tutup
</v-btn>
<v-btn color="success-600" class="text-white" variant="flat" @click="submitBooking">
<v-btn
color="success-600"
class="text-white"
variant="flat"
@click="submitBooking"
>
Simpan
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
location="top right"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon @click="snackbar = false">
@@ -263,10 +355,10 @@
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from 'vue';
import { useRoute } from '#app';
import { useAnjunganStore } from '@/stores/anjunganStore';
import { useClinicStore } from '@/stores/clinicStore';
import { ref, computed, onMounted, nextTick } from "vue";
import { useRoute } from "#app";
import { useAnjunganStore } from "@/stores/anjunganStore";
import { useClinicStore } from "@/stores/clinicStore";
definePageMeta({
layout: false,
@@ -287,54 +379,47 @@ const anjunganData = computed(() => {
const id = anjunganId.value;
if (!id) return null;
const fromGetter = typeof anjunganStore.getAnjunganById === 'function'
? anjunganStore.getAnjunganById(id)
: null;
const fromGetter =
typeof anjunganStore.getAnjunganById === "function"
? anjunganStore.getAnjunganById(id)
: null;
if (fromGetter?.value) {
return fromGetter.value;
}
const list =
anjunganStore.anjunganItems?.value ||
anjunganStore.anjunganItems ||
[];
anjunganStore.anjunganItems?.value || anjunganStore.anjunganItems || [];
if (!Array.isArray(list)) return null;
return list.find((a) => Number(a.id) === Number(id)) || null;
});
// ============================================================
// IMPROVED: API Integration dengan Manual Mapping Table
// ============================================================
// 🗺 KODE MAPPING TABLE
// Mapping dari kode 2 karakter (store) ke kode lengkap (API)
// PENTING: Sesuaikan dengan kode yang sebenarnya digunakan di API Anda
//kode mapping table
const KODE_MAPPING = {
'AN': 'ANA', // Anak
'AS': 'ANESTESI', // Anestesi
'BD': 'BEDAH', // Bedah
'GR': 'GERIATRI', // Geriatri
'GI': 'GIGI', // Gigi dan Mulut
'GZ': 'GIZI', // Gizi
'HO': 'HEMATO', // Hemato Onkologi
'IP': 'INTERNA', // IPD / Penyakit Dalam
'JT': 'JANTUNG', // Jantung / Cardiologi
'JW': 'JIWA', // Jiwa / Psikiatri
'OB': 'OBGYN', // Kandungan
'KH': 'KEMOTERAPI', // Kemoterapi
'KN': 'NYERI', // Komplementer Nyeri
'KK': 'KULIT', // Kulit Kelamin
'MT': 'MATA', // Mata
'MC': 'MCU', // MCU
'ON': 'ONKOLOGI', // Onkologi
'PR': 'PARU', // Paru
'TD': 'TINDAKAN', // R. Tindakan
'RT': 'RADIOTERAPI', // Radioterapi
'RM': 'REHAB', // Rehab Medik
'SR': 'SARAF', // Saraf / Neurologi
'TH': 'THT', // THT
AN: "ANA", // Anak
AS: "ANESTESI", // Anestesi
BD: "BEDAH", // Bedah
GR: "GERIATRI", // Geriatri
GI: "GIGI", // Gigi dan Mulut
GZ: "GIZI", // Gizi
HO: "HEMATO", // Hemato Onkologi
IP: "INTERNA", // IPD / Penyakit Dalam
JT: "JANTUNG", // Jantung / Cardiologi
JW: "JIWA", // Jiwa / Psikiatri
OB: "OBGYN", // Kandungan
KH: "KEMOTERAPI", // Kemoterapi
KN: "NYERI", // Komplementer Nyeri
KK: "KULIT", // Kulit Kelamin
MT: "MATA", // Mata
MC: "MCU", // MCU
ON: "ONKOLOGI", // Onkologi
PR: "PARU", // Paru
TD: "TINDAKAN", // R. Tindakan
RT: "RADIOTERAPI", // Radioterapi
RM: "REHAB", // Rehab Medik
SR: "SARAF", // Saraf / Neurologi
TH: "THT", // THT
};
const todayString = new Date().toISOString().substring(0, 10);
@@ -342,77 +427,91 @@ const spesialisList = ref([]);
const doctorsByKode = ref({});
const loadingDoctors = ref({});
// Load list spesialis dari API
// load list spesialis dari API
const loadSpesialisForDate = async (tanggal) => {
try {
const data = await $fetch(
`http://10.10.150.131:8088/api/jadwaldokter/tanggal/${tanggal}`
);
spesialisList.value = Array.isArray(data) ? data : [];
console.log(`✅ Loaded ${spesialisList.value.length} spesialis for ${tanggal}`);
console.log(
`✅ Loaded ${spesialisList.value.length} spesialis for ${tanggal}`
);
} catch (error) {
console.error('❌ Gagal mengambil data spesialis dari API', error);
console.error("❌ Gagal mengambil data spesialis dari API", error);
spesialisList.value = [];
}
};
// IMPROVED: Get Spesialis ID dengan Manual Mapping + Fallback
//get spesialis ID
const getSpesialisIdForKode = (kodeKlinik) => {
if (!kodeKlinik) return null;
// 1. Normalize kode menggunakan mapping table
// 1. normalize kode menggunakan mapping table
const kodeUpper = kodeKlinik.toUpperCase();
const normalizedKode = KODE_MAPPING[kodeUpper] || kodeUpper;
console.log(`🔄 Mapping: "${kodeKlinik}" → "${normalizedKode}"`);
// 2. Cari exact match dulu
// 2. cari exact match dulu
let entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || '').toUpperCase();
const apiKode = (item.Kode || item.kode || "").toUpperCase();
return apiKode === normalizedKode;
});
// 3. Fallback: startsWith untuk fleksibilitas
// 3. jika tidak ada exact match, coba prefix match
if (!entry) {
entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || '').toUpperCase();
const apiKode = (item.Kode || item.kode || "").toUpperCase();
return apiKode.startsWith(normalizedKode);
});
if (entry) {
console.log(`⚠️ Using prefix match: "${normalizedKode}" → "${entry.Kode || entry.kode}"`);
console.log(
`⚠️ Using prefix match: "${normalizedKode}" → "${
entry.Kode || entry.kode
}"`
);
}
}
const spesialisId = entry ? (entry.id || entry.ID || entry.FK_daftar_spesialis_ID) : null;
const spesialisId = entry
? entry.id || entry.ID || entry.FK_daftar_spesialis_ID
: null;
if (!spesialisId) {
console.warn(`❌ No match for: "${kodeKlinik}" (normalized: "${normalizedKode}")`);
console.log('Available API kodes:', spesialisList.value.map(s => s.Kode || s.kode));
console.warn(
`❌ No match for: "${kodeKlinik}" (normalized: "${normalizedKode}")`
);
console.log(
"Available API kodes:",
spesialisList.value.map((s) => s.Kode || s.kode)
);
} else {
console.log(`✅ Found spesialisId: ${spesialisId} for "${kodeKlinik}"`);
}
return spesialisId;
};
// IMPROVED: Load doctors dengan better error handling & caching
const loadDoctorsForKode = async (kodeKlinik) => {
if (!kodeKlinik) return;
// Cek apakah sudah ada data atau sedang loading
// cek apakah sudah ada data atau sedang loading
if (doctorsByKode.value[kodeKlinik] || loadingDoctors.value[kodeKlinik]) {
return;
}
loadingDoctors.value[kodeKlinik] = true;
const spesialisId = getSpesialisIdForKode(kodeKlinik);
if (!spesialisId) {
console.warn(`⚠️ Skip loading doctors untuk kode: ${kodeKlinik} (spesialisId not found)`);
console.warn(
`⚠️ Skip loading doctors untuk kode: ${kodeKlinik} (spesialisId not found)`
);
loadingDoctors.value[kodeKlinik] = false;
// Set empty array agar tidak retry terus-menerus
// set empty array agar tidak retry terus-menerus
doctorsByKode.value[kodeKlinik] = [];
return;
}
@@ -428,99 +527,108 @@ const loadDoctorsForKode = async (kodeKlinik) => {
const doctorNames = list
.map((d) => d.nama_lengkap || d.Nama_dokter || d.name)
.filter(Boolean);
doctorsByKode.value[kodeKlinik] = doctorNames;
console.log(`✅ Loaded ${doctorNames.length} doctors untuk ${kodeKlinik}:`, doctorNames);
console.log(
`✅ Loaded ${doctorNames.length} doctors untuk ${kodeKlinik}:`,
doctorNames
);
} catch (error) {
console.error(`❌ Gagal mengambil dokter untuk kode ${kodeKlinik}:`, error);
// Set empty array agar tidak retry
// set empty array agar tidak retry
doctorsByKode.value[kodeKlinik] = [];
} finally {
loadingDoctors.value[kodeKlinik] = false;
}
};
// IMPROVED: Prefetch dengan comprehensive logging
const prefetchDoctorsForCurrentAnjungan = async () => {
if (!anjunganData.value || !anjunganData.value.klinik) {
console.warn('⚠️ No anjungan data or klinik data available');
console.warn("⚠️ No anjungan data or klinik data available");
return;
}
const uniqueKodes = Array.from(new Set(anjunganData.value.klinik));
console.log(`🔄 Prefetching doctors for ${uniqueKodes.length} klinik:`, uniqueKodes);
console.log(
`🔄 Prefetching doctors for ${uniqueKodes.length} klinik:`,
uniqueKodes
);
await Promise.all(uniqueKodes.map((kode) => loadDoctorsForKode(kode)));
console.log('✅ Prefetch complete. Doctors by kode:', doctorsByKode.value);
console.log("✅ Prefetch complete. Doctors by kode:", doctorsByKode.value);
};
// IMPROVED: Dialog doctors dengan explicit dependency
const dialogDoctors = computed(() => {
const kode = selectedClinic.value?.kode;
if (!kode) return [];
const apiDoctors = doctorsByKode.value[kode];
// Prioritas: API > Static
// prioritas: API > Static
if (apiDoctors && apiDoctors.length > 0) {
return apiDoctors;
}
// Fallback ke static data
// fallback ke static data
const staticDoctors = selectedClinic.value?.doctors || [];
if (staticDoctors.length > 0) {
console.log(`️ Using static doctors for ${kode} (API data not available)`);
}
return staticDoctors;
});
// IMPROVED: onMounted dengan sequential loading + debug
onMounted(async () => {
console.log('🚀 Component mounted, starting data load...');
console.log("🚀 Component mounted, starting data load...");
// Step 1: Load spesialis list
await loadSpesialisForDate(todayString);
// Step 2: Wait for computed values to be ready
await nextTick();
// 🔍 DEBUG: Tampilkan comparison table
if (anjunganData.value?.klinik) {
console.group('📊 Kode Mapping Comparison');
console.group("📊 Kode Mapping Comparison");
console.table(
anjunganData.value.klinik.map(storeKode => {
anjunganData.value.klinik.map((storeKode) => {
const clinic = clinicStore.getClinicByKode(storeKode);
const normalizedKode = KODE_MAPPING[storeKode.toUpperCase()] || storeKode;
const normalizedKode =
KODE_MAPPING[storeKode.toUpperCase()] || storeKode;
const spesialisId = getSpesialisIdForKode(storeKode);
const apiMatch = spesialisList.value.find(s =>
(s.id || s.ID || s.FK_daftar_spesialis_ID) === spesialisId
const apiMatch = spesialisList.value.find(
(s) => (s.id || s.ID || s.FK_daftar_spesialis_ID) === spesialisId
);
return {
'Store Kode': storeKode,
'Clinic Name': clinic?.name || '❌ Not found',
'Normalized': normalizedKode,
'API Kode': apiMatch ? (apiMatch.Kode || apiMatch.kode) : '❌ Not matched',
'Spesialis ID': spesialisId || '❌',
'Status': spesialisId ? '✅' : '❌'
"Store Kode": storeKode,
"Clinic Name": clinic?.name || "❌ Not found",
Normalized: normalizedKode,
"API Kode": apiMatch
? apiMatch.Kode || apiMatch.kode
: "❌ Not matched",
"Spesialis ID": spesialisId || "❌",
Status: spesialisId ? "✅" : "❌",
};
})
);
console.groupEnd();
}
// Step 3: Prefetch doctors
await prefetchDoctorsForCurrentAnjungan();
console.log('✅ Initial data load complete');
console.log("✅ Initial data load complete");
});
const filteredClinics = computed(() => {
if (!anjunganData.value || !anjunganData.value.klinik || anjunganData.value.klinik.length === 0) {
if (
!anjunganData.value ||
!anjunganData.value.klinik ||
anjunganData.value.klinik.length === 0
) {
return [];
}
@@ -545,13 +653,12 @@ const bookingForm = ref({
payment: null,
});
// IMPROVED: Display doctor info dengan prioritas API
// tampilkan doctor info dengan prioritas API
const getDisplayDoctorInfo = (clinic) => {
const kode = clinic.kode;
const apiDoctors = kode ? doctorsByKode.value[kode] : null;
const doctors = (apiDoctors && apiDoctors.length > 0)
? apiDoctors
: (clinic.doctors || []);
const doctors =
apiDoctors && apiDoctors.length > 0 ? apiDoctors : clinic.doctors || [];
if (!doctors || doctors.length === 0) {
return "Tidak ada dokter";
@@ -570,7 +677,7 @@ const getDisplayDoctorInfo = (clinic) => {
const isShift1Full = (clinic) => {
if (!clinic || !clinic.shifts || clinic.shifts.length === 0) return true;
const shift1 = clinic.shifts.find(s => s.name === "Shift 1");
const shift1 = clinic.shifts.find((s) => s.name === "Shift 1");
return shift1 ? shift1.quota === 0 : true;
};
@@ -580,28 +687,30 @@ const getCurrentShiftNumber = () => {
const getCurrentShiftQuota = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return 0;
const currentShift = selectedClinic.value.shifts.find(s => s.name === "Shift 1");
const currentShift = selectedClinic.value.shifts.find(
(s) => s.name === "Shift 1"
);
return currentShift ? currentShift.quota : 0;
};
const getAvailableShiftsForBooking = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return [];
const selectedDate = bookingForm.value.date;
const today = new Date().toISOString().substring(0, 10);
const isToday = selectedDate === today;
return selectedClinic.value.shifts
.filter(shift => {
.filter((shift) => {
if (isToday) {
return shift.name !== "Shift 1" && shift.quota > 0;
} else {
return shift.quota > 0;
}
})
.map(shift => ({
.map((shift) => ({
label: `${shift.name} (Kuota: ${shift.quota} pasien)`,
value: shift.name
value: shift.name,
}));
};
@@ -638,7 +747,7 @@ const selectVisitType = (type) => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().substring(0, 10);
bookingForm.value = {
date: tomorrowStr,
shift: "",
@@ -672,7 +781,7 @@ const submitBooking = () => {
};
const backToList = () => {
navigateTo('/anjungan/anjungan');
navigateTo("/anjungan/anjungan");
};
</script>
@@ -685,7 +794,11 @@ const backToList = () => {
}
.page-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
background: linear-gradient(
135deg,
var(--color-primary-600) 0%,
var(--color-primary-700) 100%
);
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 4px 16px rgba(255, 155, 27, 0.25);
@@ -952,12 +1065,12 @@ const backToList = () => {
.dialog-info {
text-align: left;
font-size: 14px;
p {
margin: 8px 0;
color: var(--color-neutral-800);
}
strong {
color: var(--color-neutral-900);
}
@@ -965,7 +1078,7 @@ const backToList = () => {
.doctor-list-section {
margin-bottom: 12px;
p {
margin-bottom: 8px;
}
@@ -975,7 +1088,7 @@ const backToList = () => {
list-style: none;
padding: 0;
margin: 0;
li {
padding: 6px 12px;
margin: 4px 0;
@@ -988,7 +1101,7 @@ const backToList = () => {
.quota-section {
margin-bottom: 16px;
p {
margin-bottom: 12px;
font-size: 15px;
@@ -1000,19 +1113,27 @@ const backToList = () => {
flex-direction: column;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, var(--color-success-100) 0%, var(--color-success-200) 100%);
background: linear-gradient(
135deg,
var(--color-success-100) 0%,
var(--color-success-200) 100%
);
border-radius: 12px;
border: 2px solid var(--color-success-400);
margin-bottom: 12px;
&.quota-empty {
background: linear-gradient(135deg, var(--color-neutral-200) 0%, var(--color-neutral-300) 100%);
background: linear-gradient(
135deg,
var(--color-neutral-200) 0%,
var(--color-neutral-300) 100%
);
border-color: var(--color-neutral-500);
.quota-large-number {
color: var(--color-neutral-600);
}
.quota-label {
color: var(--color-neutral-700);
}
@@ -1066,20 +1187,20 @@ const backToList = () => {
flex-direction: column;
gap: 12px;
}
.header-right {
width: 100%;
}
.info-guide {
width: 100%;
}
.guide-item {
font-size: 12px;
justify-content: center;
}
.guide-number {
width: 22px;
height: 22px;
@@ -1089,11 +1210,11 @@ const backToList = () => {
.page-title {
font-size: 24px;
}
.clinic-card {
height: 170px;
}
.clinic-name {
font-size: 20px;
}
@@ -1132,11 +1253,12 @@ const backToList = () => {
.clinic-icon-wrapper .v-icon {
font-size: 32px !important;
}
.doctor-info, .schedule-info {
.doctor-info,
.schedule-info {
font-size: 11px;
}
.doctor-info {
padding-right: 60px;
}