284 lines
6.5 KiB
Vue
284 lines
6.5 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">
|
|
<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 }"
|
|
>
|
|
{{ status.label }} ({{ status.count }})
|
|
</v-chip>
|
|
</v-chip-group>
|
|
|
|
<v-text-field
|
|
v-model="searchModel"
|
|
placeholder="Cari barcode, nomor antrian..."
|
|
density="compact"
|
|
hide-details
|
|
class="search-field"
|
|
prepend-inner-icon="mdi-magnify"
|
|
/>
|
|
</div>
|
|
</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">Tidak ada data pasien</div>
|
|
<div class="empty-subtext">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
|
|
},
|
|
terlambatCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
pendingCount: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
itemsPerPage: {
|
|
type: Number,
|
|
default: 9
|
|
},
|
|
statusLabels: {
|
|
type: Object,
|
|
default: () => ({
|
|
all: 'Semua',
|
|
diloket: 'Di Loket',
|
|
terlambat: 'Terlambat',
|
|
pending: 'Pending'
|
|
})
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update:selectedStatus', 'update:searchQuery', 'action']);
|
|
|
|
const currentPage = ref(1);
|
|
|
|
const selectedStatusModel = computed({
|
|
get: () => props.selectedStatus,
|
|
set: (value) => {
|
|
currentPage.value = 1; // Reset to first page on filter change
|
|
emit('update:selectedStatus', value);
|
|
}
|
|
});
|
|
|
|
const searchModel = computed({
|
|
get: () => props.searchQuery,
|
|
set: (value) => {
|
|
currentPage.value = 1; // Reset to first page on search
|
|
emit('update:searchQuery', value);
|
|
}
|
|
});
|
|
|
|
const statusOptions = computed(() => [
|
|
{ value: 'all', label: props.statusLabels.all, count: props.items.length },
|
|
{ value: 'diloket', label: props.statusLabels.diloket, count: props.diLoketCount },
|
|
{ value: 'terlambat', label: props.statusLabels.terlambat, count: props.terlambatCount },
|
|
{ value: 'pending', label: props.statusLabels.pending, count: props.pendingCount }
|
|
]);
|
|
|
|
const filteredItems = computed(() => {
|
|
if (selectedStatusModel.value === 'all') return props.items;
|
|
return props.items.filter(p => p.status === selectedStatusModel.value);
|
|
});
|
|
|
|
const filteredAndSearchedItems = computed(() => {
|
|
if (!searchModel.value) return filteredItems.value;
|
|
|
|
const searchLower = searchModel.value.toLowerCase();
|
|
return filteredItems.value.filter(patient =>
|
|
patient.barcode?.toLowerCase().includes(searchLower) ||
|
|
patient.noAntrian?.toLowerCase().includes(searchLower) ||
|
|
patient.klinik?.toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.search-field {
|
|
max-width: 300px;
|
|
}
|
|
|
|
.patient-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 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;
|
|
}
|
|
|
|
.search-field {
|
|
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(300px, 1fr));
|
|
}
|
|
}
|
|
</style> |