Files
web-antrean/pages/Anjungan/AntrianLoket/index.vue
T

428 lines
10 KiB
Vue

<!-- pages/Anjungan/AntrianLoket/index.vue -->
<template>
<div>
<div class="selection-header">
<div class="header-icon">
<img
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
alt="RSUD Logo"
class="header-logo"
width="40"
height="40"
style="width: 40px; height: 40px; object-fit: contain;"
/>
</div>
<div class="header-content">
<h1 class="main-title">Pilih Layar Antrian Loket</h1>
<p class="subtitle">RSUD dr. Saiful Anwar Provinsi Jawa Timur</p>
</div>
</div>
<ClientOnly>
<div class="loket-selection-container">
<div v-if="visibleLokets.length > 0" class="lokets-grid">
<div
v-for="loket in visibleLokets"
:key="loket.id"
class="loket-card"
@click="navigateToLoket(loket.id)"
>
<div class="loket-card-header">
<div class="loket-info">
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
</div>
</div>
<div class="loket-preview">
<div class="loket-service-count">
<v-icon size="18" color="primary-600">mdi-hospital-building</v-icon>
<span>{{ (loket.pelayanan || []).length }} Pelayanan</span>
</div>
<div class="loket-tags">
<template v-if="loket.pelayanan && loket.pelayanan.length > 0">
<v-chip
v-for="(pelayananKode, idx) in (loket.pelayanan || []).slice(0, 3)"
:key="idx"
size="small"
class="ma-1 chip-preview"
>
{{ getKlinikName(pelayananKode, loket) }}
</v-chip>
<v-chip
v-if="(loket.pelayanan || []).length > 3"
size="small"
class="ma-1 chip-more"
>
+{{ (loket.pelayanan || []).length - 3 }}
</v-chip>
</template>
<div v-else class="text-caption text-grey">Tidak ada pelayanan terdaftar</div>
</div>
</div>
<div class="loket-card-footer">
<v-btn
color="primary-600"
variant="flat"
size="large"
block
class="btn-view text-white"
>
<v-icon left>mdi-eye</v-icon>
Tampilkan
</v-btn>
</div>
</div>
<!-- Sentinel for Infinite Scroll (placed inside grid or after it, depending on layout structure) -->
<!-- Since grid is flex/grid, we might need it to be full width or just present -->
<div ref="targetRef" style="height: 20px; width: 100%; grid-column: 1 / -1;"></div>
</div>
<div v-else class="empty-state">
<v-icon size="64" color="grey-lighten-1">mdi-view-dashboard-off</v-icon>
<h3>Tidak Ada Loket Tersedia</h3>
<p>Silakan tambah loket terlebih dahulu di halaman master</p>
<v-btn
color="primary-600"
variant="flat"
class="mt-4"
@click="navigateToSettings"
>
<v-icon left>mdi-cog</v-icon>
Ke Halaman Master
</v-btn>
</div>
</div>
<template #fallback>
<div class="d-flex justify-center align-center" style="height: 400px;">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
</div>
</template>
</ClientOnly>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue';
import { useLoketStore } from '@/stores/loketStore';
import { useMasterStore } from '@/stores/masterStore';
import { useInfiniteScroll } from '@/composables/useInfiniteScroll';
import { useRoute } from '#app';
definePageMeta({
layout: false,
});
const loketStore = useLoketStore();
const masterStore = useMasterStore();
const route = useRoute();
// Auto-fetch API loket saat component mount
onMounted(() => {
loketStore.fetchLoketFromAPI(true).then(result => {
if (result.success) {
console.log('✅ [AntrianLoket Index] Loket API data loaded:', result.message);
} else {
console.warn('⚠️ [AntrianLoket Index] Failed to fetch loket from API:', result.message);
}
}).catch(err => {
console.error('❌ [AntrianLoket Index] Error fetching loket from API:', err);
});
});
// Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst)
const numberToLetter = (num) => {
if (num < 1) return 'A';
const charCode = 64 + num;
return String.fromCharCode(Math.min(charCode, 90));
};
// Get all lokets from loketStore (master loket)
const allLokets = computed(() => {
// Akses langsung dari loketStore.loketData (ref)
const lokets = loketStore.loketData
if (!lokets) return []
// Jika lokets adalah ref, gunakan .value, jika tidak langsung return
return Array.isArray(lokets) ? lokets : (lokets.value || [])
});
// Infinite Scroll (Replaces Pagination)
const pageSize = 20;
const { visibleItems: visibleLokets, targetRef, hasMore } = useInfiniteScroll(allLokets, pageSize);
// Deprecated: Pagination logic removed
const navigateToLoket = (loketId) => {
navigateTo(`/anjungan/antrianloket/${loketId}`);
};
const navigateToSettings = () => {
navigateTo('/setting/masterloket');
};
// Helper function untuk mendapatkan nama klinik dari kode
const getKlinikName = (kode, loket = null) => {
// 1. Coba cari di _spesialisDetail (data dari API)
if (loket && loket._spesialisDetail) {
const detail = loket._spesialisDetail.find(s => String(s.idklinik) === String(kode));
if (detail && detail.namaklinik) {
return detail.namaklinik;
}
}
// 2. Coba getKlinikById untuk local data (numeric ID seperti 1000, 1001)
// Local data EKSEKUTIF menggunakan ID numeric, bukan kode
const byId = masterStore.getKlinikById ? masterStore.getKlinikById(kode) : null;
if (byId && byId.nama) return byId.nama;
// 3. Fallback ke masterStore.getKlinikNameByKode untuk data yang pakai kode
return masterStore.getKlinikNameByKode ? masterStore.getKlinikNameByKode(kode) : kode;
};
</script>
<style scoped lang="scss">
.loket-selection-container {
background: var(--color-neutral-300);
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.lokets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
width: 100%;
margin: 0;
}
@media (max-width: 1280px) {
.lokets-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 960px) {
.lokets-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
width: 54px;
height: 54px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 40px;
height: 40px;
object-fit: contain;
}
.header-content {
flex: 1;
color: var(--color-neutral-100);
}
.main-title {
font-size: 32px;
font-weight: 600;
margin: 0;
color: var(--color-neutral-100);
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.9;
line-height: 22px;
}
.loket-card {
background: var(--color-neutral-100);
border-radius: 16px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 2px solid var(--color-neutral-400);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
gap: 16px;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.2);
border-color: var(--color-primary-600);
}
}
.loket-card-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
border-bottom: 2px solid var(--color-neutral-400);
}
.loket-info {
flex: 1;
}
.loket-name {
font-size: 20px;
font-weight: 700;
margin: 0;
color: var(--color-neutral-900);
line-height: 1.2;
}
.loket-details {
font-size: 14px;
color: var(--color-neutral-600);
margin: 4px 0 0 0;
font-weight: 500;
}
.loket-preview {
flex: 1;
}
.loket-service-count {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: var(--color-primary-600);
margin-bottom: 12px;
}
.loket-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.chip-preview {
background-color: var(--color-primary-600) !important;
color: var(--color-neutral-100) !important;
font-weight: 500;
font-size: 12px;
}
.chip-more {
background-color: var(--color-neutral-600) !important;
color: var(--color-neutral-100) !important;
font-weight: 600;
font-size: 12px;
}
.loket-card-footer {
margin-top: auto;
}
.btn-view {
font-weight: 600;
text-transform: none;
letter-spacing: 0.5px;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: var(--color-neutral-700);
h3 {
font-size: 24px;
font-weight: 700;
margin: 24px 0 8px 0;
color: var(--color-neutral-800);
}
p {
font-size: 16px;
color: var(--color-neutral-600);
}
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 24px;
}
.page-info {
font-weight: 600;
color: var(--color-neutral-800);
}
@media (max-width: 768px) {
.loket-selection-container {
padding: 12px;
}
.selection-header {
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 4px;
width: 36px;
height: 36px;
}
.header-logo {
width: 24px;
height: 24px;
}
.main-title {
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 13px;
line-height: 18px;
}
.lokets-grid {
grid-template-columns: 1fr;
gap: 16px;
}
}
</style>