571 lines
14 KiB
Vue
571 lines
14 KiB
Vue
<template>
|
|
<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="selectedStatusModel" mandatory class="status-filter">
|
|
<v-chip
|
|
v-for="status in statusOptions"
|
|
:key="status.value"
|
|
: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>
|
|
</div>
|
|
|
|
<!-- Advanced Filters -->
|
|
<div class="advanced-filters mt-3">
|
|
<v-select
|
|
v-model="selectedKlinik"
|
|
:items="klinikOptions"
|
|
label="Filter Klinik"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
class="filter-select"
|
|
variant="outlined"
|
|
>
|
|
<template #prepend-inner>
|
|
<v-icon size="20">mdi-hospital-building</v-icon>
|
|
</template>
|
|
</v-select>
|
|
|
|
<v-select
|
|
v-model="selectedPembayaran"
|
|
:items="pembayaranOptions"
|
|
label="Filter Pembayaran"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
class="filter-select"
|
|
variant="outlined"
|
|
>
|
|
<template #prepend-inner>
|
|
<v-icon size="20">mdi-cash</v-icon>
|
|
</template>
|
|
</v-select>
|
|
|
|
<v-select
|
|
v-model="selectedShift"
|
|
:items="shiftOptions"
|
|
label="Filter Shift"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
class="filter-select"
|
|
variant="outlined"
|
|
>
|
|
<template #prepend-inner>
|
|
<v-icon size="20">mdi-clock-outline</v-icon>
|
|
</template>
|
|
</v-select>
|
|
|
|
<v-btn
|
|
v-if="hasActiveFilters"
|
|
variant="text"
|
|
color="primary-600"
|
|
size="small"
|
|
@click="clearAllFilters"
|
|
>
|
|
<v-icon start size="18">mdi-filter-off</v-icon>
|
|
Reset Filter
|
|
</v-btn>
|
|
</div>
|
|
|
|
<!-- Active Filter Tags -->
|
|
<div v-if="hasActiveFilters" class="active-filters mt-2">
|
|
<v-chip
|
|
v-if="selectedKlinik"
|
|
size="small"
|
|
closable
|
|
@click:close="selectedKlinik = null"
|
|
>
|
|
Klinik: {{ selectedKlinik }}
|
|
</v-chip>
|
|
<v-chip
|
|
v-if="selectedPembayaran"
|
|
size="small"
|
|
closable
|
|
@click:close="selectedPembayaran = null"
|
|
>
|
|
Pembayaran: {{ selectedPembayaran }}
|
|
</v-chip>
|
|
<v-chip
|
|
v-if="selectedShift"
|
|
size="small"
|
|
closable
|
|
@click:close="selectedShift = null"
|
|
>
|
|
{{ selectedShift }}
|
|
</v-chip>
|
|
<v-chip
|
|
v-if="selectedFastTrackModel"
|
|
size="small"
|
|
closable
|
|
@click:close="selectedFastTrackModel = null"
|
|
>
|
|
Fast Track: {{ selectedFastTrackModel }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Info -->
|
|
<div v-if="filteredAndSearchedItems.length > 0" class="results-info mb-3">
|
|
<span class="results-text">
|
|
Menampilkan {{ paginatedItems.length }} dari {{ filteredAndSearchedItems.length }} pasien
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Patient Cards Grid -->
|
|
<div v-if="filteredAndSearchedItems.length > 0" class="patient-grid">
|
|
<PatientCard
|
|
v-for="(patient, index) in paginatedItems"
|
|
:key="`${patient.barcode}-${index}`"
|
|
:patient="patient"
|
|
@action="handleAction"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="empty-state">
|
|
<v-icon size="64" color="neutral-500">mdi-account-search</v-icon>
|
|
<div class="empty-text mt-3">
|
|
{{ searchModel || hasActiveFilters ? 'Tidak ada pasien yang sesuai' : 'Tidak ada data pasien' }}
|
|
</div>
|
|
<div class="empty-subtext">
|
|
{{ searchModel || hasActiveFilters ? 'Coba ubah filter atau kata kunci pencarian' : 'Data akan muncul ketika ada pasien yang terdaftar' }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div v-if="filteredAndSearchedItems.length > itemsPerPage" class="pagination-container mt-4">
|
|
<v-pagination
|
|
v-model="currentPage"
|
|
:length="totalPages"
|
|
:total-visible="7"
|
|
rounded="circle"
|
|
/>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
import PatientCard from './PatientCard.vue';
|
|
|
|
const props = defineProps({
|
|
items: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
selectedStatus: {
|
|
type: String,
|
|
default: 'all'
|
|
},
|
|
searchQuery: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
diLoketCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
diprosesCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
terlambatCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
pendingCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
waitingCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
showDiproses: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
itemsPerPage: {
|
|
type: Number,
|
|
default: 12
|
|
},
|
|
statusLabels: {
|
|
type: Object,
|
|
default: () => ({
|
|
all: 'Semua',
|
|
diloket: 'Di Loket',
|
|
diproses: 'Diproses',
|
|
terlambat: 'Terlambat',
|
|
pending: 'Pending'
|
|
})
|
|
},
|
|
selectedFastTrack: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
fastTrackOptions: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
|
|
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 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);
|
|
}
|
|
});
|
|
|
|
const searchModel = computed({
|
|
get: () => props.searchQuery,
|
|
set: (value) => {
|
|
currentPage.value = 1;
|
|
emit('update:searchQuery', value);
|
|
}
|
|
});
|
|
|
|
const statusOptions = computed(() => {
|
|
const baseOptions = [
|
|
{ value: 'all', label: props.statusLabels.all, count: props.items.length },
|
|
{ value: 'diloket', label: props.statusLabels.diloket, count: props.diLoketCount }
|
|
];
|
|
|
|
// Tampilkan "Diproses" hanya jika:
|
|
// - label-nya didefinisikan, DAN
|
|
// - komponen mengizinkan (showDiproses = true)
|
|
if (props.showDiproses && props.statusLabels.diproses) {
|
|
baseOptions.push({
|
|
value: 'diproses',
|
|
label: props.statusLabels.diproses,
|
|
count: props.diprosesCount
|
|
});
|
|
}
|
|
|
|
// Tambahkan kategori "Menunggu" (pasien yang sudah dipanggil tapi belum check-in)
|
|
if (props.waitingCount > 0) {
|
|
baseOptions.push({
|
|
value: 'waiting',
|
|
label: 'Menunggu',
|
|
count: props.waitingCount,
|
|
icon: 'mdi-clock-outline'
|
|
});
|
|
}
|
|
|
|
baseOptions.push(
|
|
{ value: 'terlambat', label: props.statusLabels.terlambat, count: props.terlambatCount },
|
|
{ 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;
|
|
});
|
|
|
|
// Generate filter options from items
|
|
const klinikOptions = computed(() => {
|
|
const kliniks = [...new Set(props.items.map(p => p.klinik))];
|
|
return kliniks.sort();
|
|
});
|
|
|
|
const pembayaranOptions = computed(() => {
|
|
const pembayaran = [...new Set(props.items.map(p => p.pembayaran))];
|
|
return pembayaran.sort();
|
|
});
|
|
|
|
const shiftOptions = computed(() => {
|
|
const shifts = [...new Set(props.items.map(p => p.shift))];
|
|
return shifts.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 || selectedFastTrackModel.value);
|
|
});
|
|
|
|
const clearAllFilters = () => {
|
|
selectedKlinik.value = null;
|
|
selectedPembayaran.value = null;
|
|
selectedShift.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);
|
|
});
|
|
|
|
const filteredAndSearchedItems = computed(() => {
|
|
let result = filteredItems.value;
|
|
|
|
// Apply attribute filters
|
|
if (selectedKlinik.value) {
|
|
result = result.filter(p => p.klinik === selectedKlinik.value);
|
|
}
|
|
if (selectedPembayaran.value) {
|
|
result = result.filter(p => p.pembayaran === selectedPembayaran.value);
|
|
}
|
|
if (selectedShift.value) {
|
|
result = result.filter(p => p.shift === selectedShift.value);
|
|
}
|
|
// Fast Track filtering is now handled in filteredItems as a status category
|
|
|
|
// Apply search
|
|
if (searchModel.value) {
|
|
const searchLower = searchModel.value.toLowerCase();
|
|
result = result.filter(patient =>
|
|
patient.barcode?.toLowerCase().includes(searchLower) ||
|
|
patient.noAntrian?.toLowerCase().includes(searchLower) ||
|
|
patient.klinik?.toLowerCase().includes(searchLower)
|
|
);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
const totalPages = computed(() =>
|
|
Math.ceil(filteredAndSearchedItems.value.length / props.itemsPerPage)
|
|
);
|
|
|
|
const paginatedItems = computed(() => {
|
|
const start = (currentPage.value - 1) * props.itemsPerPage;
|
|
const end = start + props.itemsPerPage;
|
|
return filteredAndSearchedItems.value.slice(start, end);
|
|
});
|
|
|
|
const handleAction = (patient, action) => {
|
|
emit('action', patient, action);
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.patient-data-container {
|
|
border-radius: 12px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
background: var(--color-neutral-100);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
|
|
&: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);
|
|
}
|
|
|
|
.advanced-filters {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.filter-select {
|
|
min-width: 180px;
|
|
max-width: 220px;
|
|
}
|
|
|
|
.active-filters {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.results-info {
|
|
padding: 8px 12px;
|
|
background: var(--color-neutral-200);
|
|
border-radius: 8px;
|
|
border-left: 3px solid var(--color-primary-600);
|
|
}
|
|
|
|
.results-text {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--color-neutral-700);
|
|
}
|
|
|
|
.patient-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--color-neutral-700);
|
|
}
|
|
|
|
.empty-subtext {
|
|
font-size: 13px;
|
|
color: var(--color-neutral-600);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.pagination-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-top: 16px;
|
|
border-top: 1px solid var(--color-neutral-400);
|
|
}
|
|
|
|
:deep(.v-pagination__item) {
|
|
font-weight: 600;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.filters {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.status-filter {
|
|
min-width: 100%;
|
|
}
|
|
|
|
.advanced-filters {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.filter-select {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.patient-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 961px) and (max-width: 1264px) {
|
|
.patient-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1265px) {
|
|
.patient-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
|
|
}
|
|
}
|
|
</style> |