274 lines
12 KiB
Vue
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> |