Files
antrean-operasi/components/pendaftaran/CardAntrianListV2.vue
T
2026-03-05 13:51:16 +07:00

258 lines
11 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 }>;
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
hasMore: false,
totalItems: 0,
getActions: () => []
});
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;"
@mouseenter="$event.currentTarget.style.borderColor = '#1976D2'"
@mouseleave="$event.currentTarget.style.borderColor = ''" @click="handleActionClick('view', item)">
<v-card-text class="pa-6">
<v-row>
<v-col cols="4">
<h3 class="text-h6 font-weight-bold">
{{ item.NamaPasien }}
<Icon class="ml-1" :icon="getGenderIcon(item.JenisKelamin)"
:class="getGenderColor(item.JenisKelamin)" height="16" />
</h3>
<div class="d-flex align-center mt-2">
<Icon icon="solar:calendar-date-bold" height="14" class="mr-1 text-medium-emphasis" />
<span class="text-body-2 text-medium-emphasis" >{{ formatDate(item.TglDaftar) }}</span>
</div>
<div class="d-flex ga-2 mt-2">
<v-chip size="small" color="primary" variant="outlined" label
class="font-weight-medium">
No : {{ item.NoUrutKategori }}
</v-chip>
<v-chip size="small" color="primary" variant="outlined" label
class="font-weight-medium">
Spesialis : {{ item.NoUrutSpesialis }}
</v-chip>
<v-chip size="small" color="primary" variant="outlined" label
class="font-weight-medium">
SubSpesialis : {{ item.NoUrutSubSpesialis }}
</v-chip>
</div>
</v-col>
<v-col cols="4">
<v-row dense class="mt-6">
<!-- Left Column -->
<v-col cols="12" class="d-flex justify-space-between">
<div class="d-flex align-center">
<Icon icon="solar:card-bold" height="16" class="mr-2 text-primary" />
<span style="font-size: 13px;">{{ item.NoRekamMedis || '-' }}</span>
</div>
<div class="d-flex align-center mb-2">
<Icon icon="solar:clipboard-list-bold" height="16" class="mr-2 text-primary" />
<span style="font-size: 13px;">{{ item.Kategori.split('-')[1]?.trim() ||
item.Kategori }}</span>
</div>
</v-col>
<!-- Right Column -->
<v-col cols="12">
<div class="d-flex">
<Icon icon="solar:users-group-rounded-bold" height="16"
class="mr-2 text-primary" />
<span style="font-size: 13px;">
{{ item.Spesialis.toUpperCase() }} -
{{item.SubSpesialis.toUpperCase() }}
</span>
</div>
</v-col>
</v-row>
</v-col>
<v-col cols="4" class="d-flex flex-column justify-space-between">
<!-- Status at top right -->
<div class="d-flex justify-end">
<v-chip :color="getStatusColor(item.StatusOperasi)" size="small" variant="flat"
class="font-weight-medium" style="text-transform: uppercase;">
{{ statusLabels[item.StatusOperasi as keyof typeof statusLabels] }}
</v-chip>
</div>
<!-- Action buttons at bottom right -->
<div class="d-flex justify-end mt-5">
<v-btn-group density="compact" variant="outlined">
<v-tooltip v-for="action in getActions(item)" :key="action.event" location="top">
<template #activator="{ props }">
<v-btn v-bind="props" :color="action.color" :prepend-icon="action.icon"
size="small" variant="tonal"
@click.stop="handleActionClick(action.event, item)">
{{ action.tooltip }}
</v-btn>
</template>
{{ action.tooltip }}
</v-tooltip>
</v-btn-group>
</div>
</v-col>
</v-row>
</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>