Files
antrean-operasi/components/pendaftaran/CardAntrianList.vue
T
2026-03-09 11:08:38 +07:00

274 lines
12 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue';
import type { AntreanOperasi } from '~/types/antrean';
import { STATUS, statusLabels } from '~/types/antrean';
interface Props {
items: any[];
loading?: boolean;
hasMore?: boolean;
totalItems?: number;
getActions?: (item: any) => Array<{ icon: string; color: string; tooltip: string; event: string }>;
context?: 'all' | 'kategori' | 'spesialis' | 'subspesialis';
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
hasMore: false,
totalItems: 0,
getActions: () => [],
context: 'all'
});
const emit = defineEmits<{
(e: 'view', item: any): void;
(e: 'edit', item: any): void;
(e: 'updateStatus', item: any): void;
(e: 'delete', item: any): void;
(e: 'loadMore'): void;
}>();
const scrollContainer = ref<HTMLElement | null>(null);
const isLoadingMore = ref(false);
// Status color mapping
const getStatusColor = (status: string) => {
switch (status) {
case STATUS.BELUM:
return 'primary';
case STATUS.SELESAI:
return 'success';
case STATUS.TUNDA:
return 'warning';
case STATUS.BATAL:
return 'error';
default:
return 'grey';
}
};
// Status icon mapping
const getStatusIcon = (status: string) => {
switch (status) {
case STATUS.BELUM:
return 'solar:hourglass-line-outline';
case STATUS.SELESAI:
return 'solar:check-circle-bold';
case STATUS.TUNDA:
return 'solar:pause-circle-bold';
case STATUS.BATAL:
return 'solar:close-circle-bold';
default:
return 'solar:question-circle-bold';
}
};
// Gender icon and color
const getGenderIcon = (gender: string) => {
return gender === 'L' ? 'mdi-gender-male' : 'mdi-gender-female';
};
const getGenderColor = (gender: string) => {
return gender === 'L' ? 'text-info' : 'text-error';
};
const getGenderText = (gender: string) => {
return gender === 'L' ? 'Laki-laki' : 'Perempuan';
};
// Format date
const formatDate = (date: string) => {
if (!date) return '-';
return new Date(date).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Handle action click
const handleActionClick = (action: string, item: any) => {
emit(action as any, item);
};
// Infinite scroll implementation
const handleScroll = () => {
if (!scrollContainer.value || props.loading || isLoadingMore.value || !props.hasMore) return;
const element = scrollContainer.value;
const scrollPosition = element.scrollTop + element.clientHeight;
const scrollHeight = element.scrollHeight;
// Load more when 200px from bottom
if (scrollHeight - scrollPosition < 200) {
isLoadingMore.value = true;
emit('loadMore');
// Reset loading state after a short delay
setTimeout(() => {
isLoadingMore.value = false;
}, 500);
}
};
onMounted(() => {
if (scrollContainer.value) {
scrollContainer.value.addEventListener('scroll', handleScroll);
}
});
onUnmounted(() => {
if (scrollContainer.value) {
scrollContainer.value.removeEventListener('scroll', handleScroll);
}
});
</script>
<template>
<div ref="scrollContainer" class="card-list-container"
style="max-height: calc(100vh - 200px); overflow-y: auto; overflow-x: hidden;">
<!-- Loading state -->
<div v-if="loading && items.length === 0" class="d-flex justify-center align-center pa-8"
style="min-height: 400px;">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
</div>
<!-- Empty state -->
<div v-else-if="items.length === 0" class="text-center pa-8" style="min-height: 400px;">
<Icon icon="solar:document-text-outline" height="80" class="text-medium-emphasis mb-4" />
<h3 class="text-h6 text-medium-emphasis mb-2">Tidak ada data</h3>
<p class="text-body-2 text-medium-emphasis">Belum ada antrian operasi yang terdaftar</p>
</div>
<!-- Card list -->
<v-row v-else dense>
<v-col v-for="item in items" :key="item.id" cols="12" class="pa-1">
<v-card class="card-item" elevation="0" border style="transition: all 0.2s ease; cursor: pointer;"
@mouseenter="$event.currentTarget.style.borderColor = '#1976D2'"
@mouseleave="$event.currentTarget.style.borderColor = ''" @click="handleActionClick('view', item)">
<!-- Header with queue numbers -->
<!-- <div class="d-flex justify-space-between ga-2 pa-3 pb-0 ">
<div class="d-flex ga-2">
<v-chip size="x-small" color="primary" variant="outlined" label>
No : {{ item.NoUrutKategori }}
</v-chip>
<v-chip size="x-small" color="primary" variant="outlined" label>
Spesialis : {{ item.NoUrutSpesialis }}
</v-chip>
<v-chip size="x-small" color="primary" variant="outlined" label>
SubSpesialis : {{ item.NoUrutSubSpesialis }}
</v-chip>
</div>
<v-chip :color="getStatusColor(item.StatusOperasi)" size="small" variant="flat"
class="font-weight-medium">
{{ statusLabels[item.StatusOperasi as keyof typeof statusLabels] }}
</v-chip>
</div> -->
<v-card-text class="pa-6">
<div class="d-flex ga-4" style="position: relative;">
<!-- Gender Avatar -->
<!-- <v-tooltip location="top">
<template #activator="{ props }">
<v-avatar v-bind="props" :color="getGenderColor(item.JenisKelamin)" size="80"
class="elevation-2">
<span class="text-h4 font-weight-bold">
{{ item.JenisKelamin == 'L' ? 'L' : 'P' }}
</span>
</v-avatar>
</template>
{{ getGenderText(item.JenisKelamin) }}
</v-tooltip> -->
<!-- Patient Info Section -->
<div class="flex-grow-1">
<div class="d-flex justify-space-between">
<h3 class="text-h6 font-weight-bold mb-3">{{ item.NamaPasien }}
<Icon class="ml-1" :icon="getGenderIcon(item.JenisKelamin)"
:class="getGenderColor(item.JenisKelamin)" height="14" />
</h3>
<v-chip :color="getStatusColor(item.StatusOperasi)" size="small" variant="flat"
class="font-weight-medium">
{{ statusLabels[item.StatusOperasi as keyof typeof statusLabels] }}
</v-chip>
</div>
<div class="d-flex ga-2 mb-4">
<!-- Show all numbers for 'all' and 'kategori' context -->
<v-chip v-if="context === 'all' || context === 'kategori'" size="small" color="primary" variant="outlined" label class="font-weight-medium">
No : {{ item.NoUrutKategori }}
</v-chip>
<!-- Show spesialis number for 'all', 'kategori', 'spesialis' context -->
<v-chip v-if="context === 'all' || context === 'kategori' || context === 'spesialis'" size="small" color="primary" variant="outlined" label class="font-weight-medium">
No Spesialis : {{ item.NoUrutSpesialis }}
</v-chip>
<!-- Show subspesialis number for all contexts -->
<v-chip size="small" color="primary" variant="outlined" label class="font-weight-medium">
No Sub Spesialis : {{ item.NoUrutSubSpesialis }}
</v-chip>
</div>
<!-- Medical Details Grid -->
<v-row dense class="mt-2">
<v-col cols="3">
<div class="d-flex align-center mb-2">
<Icon icon="solar:card-bold" height="16" class="mr-2 text-primary" />
<span class="text-body-1">{{ item.NoRekamMedis || '-' }}</span>
</div>
<div class="d-flex align-center mb-2">
<Icon icon="solar:calendar-bold" height="16" class="mr-2 text-primary" />
<span class="text-body-1">{{ formatDate(item.TglDaftar) }}</span>
</div>
</v-col>
<v-col cols="5">
<div class="d-flex align-center mb-2">
<Icon icon="solar:clipboard-list-bold" height="16" class="mr-2 text-primary" />
<span class="text-body-1">{{ item.Kategori.split('-')[1]?.trim() || item.Kategori }}</span>
</div>
<div class="d-flex align-center mb-2">
<Icon icon="solar:users-group-rounded-bold" height="16" class="mr-2 text-primary" />
<span class="text-body-1">{{ item.Spesialis.toUpperCase() }} - {{ item.SubSpesialis.toUpperCase() }}</span>
</div>
</v-col>
<v-col cols="4" class="d-flex justify-end align-end">
<v-btn-group density="compact" variant="outlined">
<template v-for="action in getActions(item)" :key="action.event">
<v-btn :color="action.color"
:prepend-icon="action.icon" size="small" variant="tonal"
@click.stop="handleActionClick(action.event, item)">
{{ action.tooltip }}
</v-btn>
</template>
</v-btn-group>
</v-col>
</v-row>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Load more indicator -->
<div v-if="(isLoadingMore || loading) && items.length > 0" class="d-flex justify-center pa-4">
<v-progress-circular indeterminate color="primary" size="40"></v-progress-circular>
</div>
<!-- End of list indicator -->
<div v-if="!hasMore && items.length > 0" class="text-center pa-4">
<p class="text-body-2 text-medium-emphasis">
<Icon icon="solar:check-circle-bold" height="16" class="mr-1" />
Semua data telah dimuat ({{ items.length }} dari {{ totalItems || items.length }})
</p>
</div>
<!-- Items count indicator -->
<div v-if="hasMore && items.length > 0 && !loading" class="text-center pa-4">
<p class="text-caption text-medium-emphasis">
Menampilkan {{ items.length }} dari {{ totalItems || items.length }} data
</p>
</div>
</div>
</template>