Merge branch 'Antrean-Code' of https://git.rssa.top/arie.bagus.2905/web-antrean into Antrean-Code
This commit is contained in:
@@ -9,12 +9,11 @@
|
||||
src="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
|
||||
alt="RSUD Logo"
|
||||
class="header-logo"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="hospital-name">ANTREAN MASUK</h1>
|
||||
<p class="display-subtitle">RSUD dr. Saiful Anwar Provinsi Jawa Timur</p>
|
||||
<p v-if="loketData" class="klinik-info">{{ loketData.namaLoket }}</p>
|
||||
<h1 class="hospital-name">RSUD dr. Saiful Anwar Provinsi Jawa Timur</h1>
|
||||
<!-- <p class="display-subtitle">RSUD dr. Saiful Anwar Provinsi Jawa Timur</p> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
@@ -25,50 +24,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Grid - Display All Called Queues -->
|
||||
<div class="queue-grid-container">
|
||||
<!-- Loket Header -->
|
||||
<div class="loket-header">
|
||||
<h2 class="loket-title">{{ loketData?.namaLoket || 'LOKET A' }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Patient Cards Grid (like AdminLoket) -->
|
||||
<div v-if="allCalledQueues.length > 0" class="patient-cards-grid">
|
||||
<v-card
|
||||
v-for="queue in allCalledQueues"
|
||||
:key="queue.no"
|
||||
class="patient-card-display"
|
||||
elevation="2"
|
||||
<!-- Main Grid - Display Loket Cards -->
|
||||
<div class="loket-grid-container">
|
||||
<!-- Loket Grid -->
|
||||
<div class="lokets-grid" :style="gridStyle">
|
||||
<div
|
||||
v-for="loketData in displayedLokets"
|
||||
:key="loketData.id"
|
||||
class="loket-card"
|
||||
>
|
||||
<v-card-text class="card-text-content">
|
||||
<!-- Queue Number - Large -->
|
||||
<div class="card-content">
|
||||
<div class="queue-number-large">
|
||||
{{ queue.noAntrian.split(" |")[0] }}
|
||||
</div>
|
||||
|
||||
<!-- Fast Track Icon -->
|
||||
<div v-if="queue.fastTrack === 'YA'" class="fast-track-badge">
|
||||
<v-icon color="warning" size="36" class="fast-track-icon">
|
||||
mdi-flash
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- Klinik Info -->
|
||||
<div class="klinik-info">
|
||||
<v-chip size="default" variant="outlined" class="klinik-chip-large">
|
||||
{{ queue.klinik }}
|
||||
</v-chip>
|
||||
<!-- Loket Header -->
|
||||
<div class="loket-header-bar">
|
||||
<div class="loket-name">{{ loketData.nama }}</div>
|
||||
<div class="queue-count">
|
||||
<v-icon size="18" color="white">mdi-account-multiple</v-icon>
|
||||
<span class="count-number">{{ loketData.totalQueues }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Display - Vertical Scrollable -->
|
||||
<div
|
||||
:ref="el => setQueueDisplayRef(loketData.id, el)"
|
||||
class="queue-display"
|
||||
:class="{ 'has-scroll': loketData.queues.length > 20 }"
|
||||
>
|
||||
<!-- Ticket Cards List -->
|
||||
<div v-if="loketData.queues.length > 0" class="ticket-cards-list" :style="ticketGridStyle">
|
||||
<div
|
||||
v-for="(queue, index) in loketData.queues"
|
||||
:key="`${loketData.id}-${queue.no}-${queue.barcode || index}`"
|
||||
class="ticket-card"
|
||||
:class="{ 'ticket-card-fast-track': queue.fastTrack === 'YA' }"
|
||||
>
|
||||
<!-- Fast Track Badge - Pojok Kanan Atas -->
|
||||
<div v-if="queue.fastTrack === 'YA'" class="ticket-fast-track">
|
||||
<v-icon color="white" size="14">mdi-flash</v-icon>
|
||||
</div>
|
||||
<div class="ticket-number">{{ queue.noAntrian?.split(" |")[0] || queue.no || '' }}</div>
|
||||
<!-- <div v-if="queue.klinik" class="ticket-klinik">{{ queue.klinik }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="64" color="grey-lighten-3">mdi-clock-outline</v-icon>
|
||||
<p class="empty-text">Tidak Ada Antrian yang Dipanggil</p>
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else
|
||||
class="empty-queue"
|
||||
>
|
||||
<v-icon size="40" color="grey-lighten-3">mdi-minus-circle-outline</v-icon>
|
||||
<div class="empty-text">Tidak Ada Antrian</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-divider" />
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon stat-icon-waiting">
|
||||
@@ -96,7 +102,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-divider" />
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon stat-icon-active">
|
||||
@@ -117,9 +123,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useMasterStore } from '@/stores/masterStore'
|
||||
import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore'
|
||||
import { useRoute } from '#app'
|
||||
|
||||
definePageMeta({
|
||||
@@ -129,8 +136,10 @@ definePageMeta({
|
||||
const route = useRoute()
|
||||
const queueStore = useQueueStore()
|
||||
const masterStore = useMasterStore()
|
||||
const antreanMasukScreenStore = useAntreanMasukScreenStore()
|
||||
|
||||
const loketId = computed(() => {
|
||||
// Get screen ID from route (not loket ID)
|
||||
const screenId = computed(() => {
|
||||
const id = route.params.id
|
||||
// Handle both string and array cases
|
||||
const idValue = Array.isArray(id) ? id[0] : id
|
||||
@@ -138,26 +147,51 @@ const loketId = computed(() => {
|
||||
return isNaN(parsed) ? null : parsed
|
||||
})
|
||||
|
||||
const loketData = computed(() => {
|
||||
const id = loketId.value
|
||||
// Get screen configuration
|
||||
const screenData = computed(() => {
|
||||
const id = screenId.value
|
||||
if (!id) return null
|
||||
|
||||
return masterStore.getLoketById(id) || null
|
||||
|
||||
const screen = antreanMasukScreenStore.antreanMasukScreenItems.find(s => s.id === id)
|
||||
return screen || null
|
||||
})
|
||||
|
||||
// Get all loket IDs configured for this screen
|
||||
const configuredLoketIds = computed(() => {
|
||||
return screenData.value?.loket || []
|
||||
})
|
||||
|
||||
|
||||
const currentTime = ref('')
|
||||
const currentDate = ref('')
|
||||
let timeInterval = null
|
||||
|
||||
// Refs for queue display containers (for auto-scroll)
|
||||
const queueDisplayRefs = ref({})
|
||||
|
||||
const setQueueDisplayRef = (loketId, el) => {
|
||||
if (el) {
|
||||
queueDisplayRefs.value[loketId] = el
|
||||
}
|
||||
}
|
||||
|
||||
// Get all patients with processStage 'loket'
|
||||
// Force reactivity by accessing the computed result directly
|
||||
const loketPatients = computed(() => {
|
||||
try {
|
||||
const result = queueStore.getPatientsByStage('loket')
|
||||
if (result && result.value && Array.isArray(result.value.all)) {
|
||||
return result.value.all
|
||||
// Get the computed result from store
|
||||
const stageResult = queueStore.getPatientsByStage('loket')
|
||||
|
||||
// Access .value to get the actual computed value
|
||||
// This ensures reactivity after store hydration
|
||||
if (stageResult && stageResult.value) {
|
||||
const patients = stageResult.value.all || []
|
||||
// Ensure it's an array and filter out any null/undefined
|
||||
return Array.isArray(patients) ? patients.filter(p => p != null) : []
|
||||
}
|
||||
return []
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Error getting loket patients:', error)
|
||||
return []
|
||||
}
|
||||
})
|
||||
@@ -166,14 +200,12 @@ const loketPatients = computed(() => {
|
||||
// These are tickets printed from anjungan that have been called by admin loket
|
||||
// Status berubah dari 'menunggu' menjadi 'waiting' ketika dipanggil oleh admin loket
|
||||
// Status 'waiting' = sudah dipanggil tapi belum check-in
|
||||
// Setiap loket menampilkan data pasien yang berbeda berdasarkan loketId
|
||||
// Filter berdasarkan loket yang dikonfigurasi di screen
|
||||
const calledForCheckIn = computed(() => {
|
||||
const currentLoketId = loketId.value
|
||||
const loketIds = configuredLoketIds.value
|
||||
|
||||
// Untuk saat ini, hanya loket 1 yang aktif
|
||||
// Nanti setiap loket akan menampilkan data yang berbeda
|
||||
if (!currentLoketId || currentLoketId !== 1) {
|
||||
// Jika bukan loket 1, return empty array (untuk sementara)
|
||||
// Jika tidak ada loket yang dikonfigurasi, return empty array
|
||||
if (!loketIds || loketIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -183,51 +215,127 @@ const calledForCheckIn = computed(() => {
|
||||
// and not yet checked in (processStage still 'loket')
|
||||
const isCalled = p.status === 'waiting' && p.processStage === 'loket'
|
||||
|
||||
// Tampilkan semua pasien dengan status 'waiting', tidak hanya dari anjungan
|
||||
// Semua pasien yang sudah dipanggil oleh admin loket harus ditampilkan
|
||||
// Filter berdasarkan loketId yang dikonfigurasi di screen
|
||||
const patientLoketId = p.loketId || 1 // Default ke loket 1 jika tidak ada
|
||||
const isForConfiguredLoket = loketIds.includes(patientLoketId)
|
||||
|
||||
// Filter berdasarkan loketId (untuk saat ini hanya loket 1)
|
||||
// Nanti bisa ditambahkan field loketId di data pasien untuk filter yang lebih spesifik
|
||||
const isForThisLoket = !p.loketId || p.loketId === currentLoketId
|
||||
|
||||
return isCalled && isForThisLoket
|
||||
return isCalled && isForConfiguredLoket
|
||||
})
|
||||
// Tidak perlu sorting - tampilkan semua sesuai urutan dari store
|
||||
})
|
||||
|
||||
// Removed displayedQueues - tidak diperlukan lagi karena menggunakan allCalledQueues
|
||||
|
||||
// Get all called queues (tidak perlu urut, tampilkan semua)
|
||||
// Maksimal 20 orang yang dipanggil
|
||||
const allCalledQueues = computed(() => {
|
||||
try {
|
||||
// Ensure calledForCheckIn is an array
|
||||
const queues = Array.isArray(calledForCheckIn.value) ? calledForCheckIn.value : []
|
||||
// Tampilkan maksimal 20 yang sudah dipanggil
|
||||
// Tidak perlu sorting, tampilkan sesuai urutan dari store
|
||||
return queues.slice(0, 20)
|
||||
} catch {
|
||||
// Group queues by loketId and create loket cards
|
||||
const displayedLokets = computed(() => {
|
||||
const loketIds = configuredLoketIds.value
|
||||
|
||||
if (!loketIds || loketIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Ensure calledForCheckIn is an array
|
||||
const queues = Array.isArray(calledForCheckIn.value) ? calledForCheckIn.value : []
|
||||
|
||||
// Group queues by loketId
|
||||
const queuesByLoket = new Map()
|
||||
|
||||
loketIds.forEach(loketId => {
|
||||
const loket = masterStore.getLoketById(loketId)
|
||||
const loketQueues = queues
|
||||
.filter(q => {
|
||||
const patientLoketId = q.loketId || 1
|
||||
return patientLoketId === loketId
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Sort by createdAt or no for consistent ordering
|
||||
if (a.createdAt && b.createdAt) {
|
||||
return new Date(a.createdAt) - new Date(b.createdAt)
|
||||
}
|
||||
return (a.no || 0) - (b.no || 0)
|
||||
})
|
||||
|
||||
queuesByLoket.set(loketId, {
|
||||
id: loketId,
|
||||
nama: loket ? loket.namaLoket : `Loket ${loketId}`,
|
||||
queues: loketQueues,
|
||||
totalQueues: loketQueues.length
|
||||
})
|
||||
})
|
||||
|
||||
// Filter: hanya tampilkan loket yang memiliki antrian
|
||||
return Array.from(queuesByLoket.values()).filter(loket => loket.queues.length > 0)
|
||||
})
|
||||
|
||||
// Calculate grid columns based on number of displayed lokets
|
||||
const gridColumns = computed(() => {
|
||||
const count = displayedLokets.value.length
|
||||
// Jika hanya 1-2 loket, tampilkan 1 kolom (full width)
|
||||
if (count <= 2) return 1
|
||||
// Jika 3-4 loket, tampilkan 2 kolom
|
||||
if (count <= 4) return 2
|
||||
// Jika 5-6 loket, tampilkan 3 kolom
|
||||
if (count <= 6) return 3
|
||||
// Jika 7-9 loket, tampilkan 4 kolom
|
||||
if (count <= 9) return 4
|
||||
// Jika 10-12 loket, tampilkan 5 kolom
|
||||
if (count <= 12) return 5
|
||||
// Jika 13-14 loket, tampilkan 6 kolom
|
||||
if (count <= 14) return 6
|
||||
// Default: 7 kolom untuk 15+ loket
|
||||
return 7
|
||||
})
|
||||
|
||||
const gridStyle = computed(() => {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${gridColumns.value}, 1fr)`
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate ticket grid columns based on loket grid columns
|
||||
// When loket is wider (fewer columns), tickets can form more columns
|
||||
const ticketGridColumns = computed(() => {
|
||||
const loketCols = gridColumns.value
|
||||
// Jika hanya 1 kolom loket (full width), tiket bisa 3 kolom
|
||||
if (loketCols === 1) return 3
|
||||
// Jika 2 kolom loket, tiket bisa 2 kolom
|
||||
if (loketCols === 2) return 2
|
||||
// Jika 3 kolom loket, tiket bisa 2 kolom
|
||||
if (loketCols === 3) return 2
|
||||
// Jika 4+ kolom loket, tiket 1 kolom (vertikal)
|
||||
return 1
|
||||
})
|
||||
|
||||
const ticketGridStyle = computed(() => {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${ticketGridColumns.value}, 1fr)`
|
||||
}
|
||||
})
|
||||
|
||||
const statistics = computed(() => {
|
||||
// Ensure we always return default values even if data is not ready
|
||||
try {
|
||||
const currentLoketId = loketId.value
|
||||
const loketIds = configuredLoketIds.value
|
||||
|
||||
// Jika tidak ada loket yang dikonfigurasi, return 0
|
||||
if (!loketIds || loketIds.length === 0) {
|
||||
return {
|
||||
total: 0,
|
||||
waiting: 0,
|
||||
active: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure loketPatients is an array
|
||||
const patients = Array.isArray(loketPatients.value) ? loketPatients.value : []
|
||||
|
||||
// Filter semua pasien untuk loket ini (tidak hanya dari anjungan)
|
||||
// Untuk saat ini, hanya loket 1 yang aktif
|
||||
// Filter semua pasien untuk loket-loket yang dikonfigurasi di screen
|
||||
const allLoketTickets = patients.filter(p => {
|
||||
if (!p) return false
|
||||
|
||||
// Filter berdasarkan loketId (untuk saat ini hanya loket 1)
|
||||
const isForThisLoket = !p.loketId || p.loketId === currentLoketId
|
||||
|
||||
return isForThisLoket
|
||||
// Filter berdasarkan loketId yang dikonfigurasi
|
||||
const patientLoketId = p.loketId || 1 // Default ke loket 1 jika tidak ada
|
||||
return loketIds.includes(patientLoketId)
|
||||
})
|
||||
|
||||
// Menunggu = belum dipanggil (status 'menunggu')
|
||||
@@ -269,30 +377,142 @@ const updateTime = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Auto-scroll setiap 2 detik dengan loop (atas -> bawah -> atas)
|
||||
let scrollIntervals = {}
|
||||
|
||||
const startAutoScroll = () => {
|
||||
// Clear existing intervals
|
||||
Object.values(scrollIntervals).forEach(interval => {
|
||||
if (interval) clearInterval(interval)
|
||||
})
|
||||
scrollIntervals = {}
|
||||
|
||||
nextTick(() => {
|
||||
displayedLokets.value.forEach(loketData => {
|
||||
const container = queueDisplayRefs.value[loketData.id]
|
||||
if (container && loketData.queues.length > 0) {
|
||||
let isScrollingDown = true
|
||||
let currentScrollTop = 0
|
||||
|
||||
scrollIntervals[loketData.id] = setInterval(() => {
|
||||
const maxScroll = container.scrollHeight - container.clientHeight
|
||||
|
||||
// Jika konten tidak cukup untuk di-scroll, skip
|
||||
if (maxScroll <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Hitung ukuran scroll berdasarkan tinggi card
|
||||
// Ambil tinggi card pertama jika ada, atau gunakan default 80px (min-height ticket-card)
|
||||
let scrollAmount = 80 // Default: min-height ticket-card
|
||||
const firstCard = container.querySelector('.ticket-card')
|
||||
if (firstCard) {
|
||||
const cardRect = firstCard.getBoundingClientRect()
|
||||
const cardHeight = cardRect.height
|
||||
// Tambahkan gap (10px dari .ticket-cards-list gap)
|
||||
scrollAmount = cardHeight + 10
|
||||
}
|
||||
|
||||
if (isScrollingDown) {
|
||||
// Scroll ke bawah dengan ukuran card
|
||||
currentScrollTop += scrollAmount
|
||||
if (currentScrollTop >= maxScroll) {
|
||||
// Sudah sampai bawah, kembali ke atas dan lanjut scroll ke bawah
|
||||
currentScrollTop = 0
|
||||
// Tetap isScrollingDown = true agar langsung scroll ke bawah lagi
|
||||
}
|
||||
} else {
|
||||
// Scroll ke atas (tidak digunakan karena langsung reset ke atas)
|
||||
currentScrollTop -= scrollAmount
|
||||
if (currentScrollTop <= 0) {
|
||||
currentScrollTop = 0
|
||||
isScrollingDown = true
|
||||
}
|
||||
}
|
||||
|
||||
container.scrollTop = currentScrollTop
|
||||
}, 2000) // Setiap 2 detik
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Watch loketPatients to ensure reactivity after refresh
|
||||
watch(loketPatients, (newPatients) => {
|
||||
// Force update by accessing the computed
|
||||
// This ensures reactivity after store hydration
|
||||
if (newPatients && newPatients.length > 0) {
|
||||
console.log('Loket patients updated:', newPatients.length)
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
// Watch store's getPatientsByStage to ensure reactivity after refresh
|
||||
watch(() => {
|
||||
try {
|
||||
const result = queueStore.getPatientsByStage('loket')
|
||||
return result?.value?.all || []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}, (newPatients) => {
|
||||
// Force reactivity update
|
||||
if (newPatients && newPatients.length > 0) {
|
||||
console.log('Store loket patients updated:', newPatients.length)
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
// Watch displayedLokets untuk restart auto-scroll
|
||||
watch(displayedLokets, () => {
|
||||
startAutoScroll()
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
// Wait for stores to be hydrated from persisted state
|
||||
// Use nextTick to ensure stores are ready
|
||||
nextTick(() => {
|
||||
// Redirect to index if loket not found
|
||||
if (!loketData.value) {
|
||||
// Redirect to index if screen not found
|
||||
if (!screenData.value) {
|
||||
navigateTo('/anjungan/antreanmasuk')
|
||||
return
|
||||
}
|
||||
|
||||
// Untuk saat ini, hanya loket 1 yang aktif
|
||||
// Jika mengakses loket selain 1, redirect ke loket 1
|
||||
if (loketId.value !== 1) {
|
||||
navigateTo('/anjungan/antreanmasuk/1')
|
||||
return
|
||||
}
|
||||
// Force reactivity by accessing store data after delays
|
||||
// This ensures store is fully hydrated after refresh
|
||||
setTimeout(() => {
|
||||
// Access loketPatients to trigger computed
|
||||
const _ = loketPatients.value
|
||||
// Access calledForCheckIn to trigger computed
|
||||
const __ = calledForCheckIn.value
|
||||
// Access displayedLokets to trigger computed
|
||||
const ___ = displayedLokets.value
|
||||
}, 100)
|
||||
|
||||
// Additional delay to ensure store hydration is complete
|
||||
setTimeout(() => {
|
||||
// Force re-computation by accessing again
|
||||
const _ = loketPatients.value
|
||||
const __ = calledForCheckIn.value
|
||||
const ___ = displayedLokets.value
|
||||
}, 300)
|
||||
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
// Start auto-scroll setelah delay singkat
|
||||
setTimeout(() => {
|
||||
startAutoScroll()
|
||||
}, 500)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
|
||||
// Clear all scroll intervals
|
||||
Object.values(scrollIntervals).forEach(interval => {
|
||||
if (interval) clearInterval(interval)
|
||||
})
|
||||
scrollIntervals = {}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -432,160 +652,208 @@ onUnmounted(() => {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* ========== QUEUE GRID CONTAINER ========== */
|
||||
.queue-grid-container {
|
||||
/* ========== LOKET GRID CONTAINER ========== */
|
||||
.loket-grid-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--color-primary-200);
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 280px);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.08);
|
||||
}
|
||||
|
||||
.loket-header {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
||||
padding: 16px 20px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
height: 70px;
|
||||
.lokets-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
|
||||
.loket-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #E0E0E0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-height: 400px;
|
||||
/* Calculate: viewport - header (~120px) - footer (100px) - container padding (48px) - margins (44px) */
|
||||
max-height: calc(100vh - 312px);
|
||||
}
|
||||
|
||||
.loket-header .loket-title {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: #FFFFFF;
|
||||
margin: 0;
|
||||
/* Loket Header */
|
||||
.loket-header-bar {
|
||||
background: #FFFFFF;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 3px solid #FF9B1B;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loket-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #212121;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Patient Cards Grid - Fixed for 1920x1080, 5 columns */
|
||||
.patient-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-content: start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.patient-card-display {
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--color-primary-200);
|
||||
background: #FFFFFF;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.08);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.patient-card-display:hover {
|
||||
box-shadow: 0 8px 24px rgba(25, 118, 210, 0.15);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.card-text-content {
|
||||
padding: 20px !important;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.queue-number-large {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
color: #212121;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.fast-track-badge {
|
||||
.queue-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--color-primary-600);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
min-width: 50px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fast-track-icon {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
.queue-count .v-icon {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.klinik-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.klinik-chip-large {
|
||||
font-size: 14px;
|
||||
.count-number {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding: 6px 14px;
|
||||
border: 2px solid var(--color-primary-600);
|
||||
color: var(--color-primary-600);
|
||||
height: auto;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: rgba(25, 118, 210, 0.05);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
/* Queue Display - Vertical Scrollable */
|
||||
.queue-display {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
padding-bottom: 120px; /* Extra padding to prevent footer overlap */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #FFFFFF;
|
||||
min-height: 0;
|
||||
scroll-behavior: smooth;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.queue-display.has-scroll {
|
||||
max-height: calc(100vh - 400px);
|
||||
}
|
||||
|
||||
/* Hide scrollbar */
|
||||
.queue-display {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.queue-display::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
.ticket-cards-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding-bottom: 20px; /* Additional bottom padding for last card */
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
background: #F5F7FA ;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 80px;
|
||||
position: relative; /* Untuk absolute positioning fast track icon */
|
||||
}
|
||||
|
||||
.ticket-card-fast-track {
|
||||
background: rgba(255, 132, 65, 0.4) !important; /* #FF8441 dengan 40% opacity (66 hex = 102/255) */
|
||||
}
|
||||
|
||||
.ticket-card:hover {
|
||||
box-shadow: 0 2px 6px rgba(25, 118, 210, 0.2);
|
||||
border-color: var(--color-primary-600);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ticket-number {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #3556AE;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ticket-klinik {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #717171;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.ticket-fast-track {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(239, 83, 80, 0.15); /* Red background dengan opacity */
|
||||
border: 2px solid rgba(229, 57, 53, 0.4); /* Red border */
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(229, 57, 53, 0.2);
|
||||
}
|
||||
|
||||
.ticket-fast-track:hover {
|
||||
background: #E53935; /* Solid red on hover */
|
||||
border-color: #E53935;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(229, 57, 53, 0.4);
|
||||
}
|
||||
|
||||
.ticket-fast-track .v-icon {
|
||||
color: #E53935 !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ticket-fast-track:hover .v-icon {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.empty-queue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.empty-queue .v-icon {
|
||||
color: #BDBDBD !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
color: #89939E;
|
||||
margin-top: 12px;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== FOOTER STATS ========== */
|
||||
@@ -759,27 +1027,19 @@ onUnmounted(() => {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.loket-header-bar {
|
||||
padding: 20px 24px;
|
||||
.lokets-grid {
|
||||
gap: 12px;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
|
||||
.loket-title {
|
||||
.loket-name {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ticket-number {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.loket-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.current-number {
|
||||
font-size: 96px;
|
||||
}
|
||||
|
||||
.next-item {
|
||||
font-size: 24px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.footer-stats-bar {
|
||||
padding: 16px 32px;
|
||||
}
|
||||
@@ -842,53 +1102,41 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.patient-cards-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
.lokets-grid {
|
||||
gap: 12px;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
|
||||
.queue-number-large {
|
||||
font-size: 64px;
|
||||
.ticket-number {
|
||||
font-size: 20px;
|
||||
color: #3556AE;
|
||||
}
|
||||
|
||||
.patient-card-display {
|
||||
min-height: 240px;
|
||||
.ticket-card {
|
||||
min-height: 70px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 961px) and (max-width: 1400px) {
|
||||
.patient-cards-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.queue-number-large {
|
||||
font-size: 80px;
|
||||
.lokets-grid {
|
||||
gap: 12px;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1401px) and (max-width: 1919px) {
|
||||
.patient-cards-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
}
|
||||
|
||||
.queue-number-large {
|
||||
font-size: 96px;
|
||||
.lokets-grid {
|
||||
gap: 14px;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fixed layout for 1920x1080 */
|
||||
/* Fixed layout for 1920x1080 and above */
|
||||
@media (min-width: 1920px) {
|
||||
.patient-cards-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.queue-number-large {
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
.patient-card-display {
|
||||
min-height: 280px;
|
||||
.lokets-grid {
|
||||
gap: 16px;
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,12 +1149,13 @@ onUnmounted(() => {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.loket-header .loket-title {
|
||||
font-size: 28px;
|
||||
.loket-name {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.queue-grid-container {
|
||||
padding: 16px;
|
||||
.ticket-number {
|
||||
font-size: 20px;
|
||||
color: #3556AE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
<!-- pages/Anjungan/AntreanMasuk/index.vue -->
|
||||
<template>
|
||||
<div class="loket-selection-container">
|
||||
<div class="screen-selection-container">
|
||||
<div class="selection-header">
|
||||
<div class="header-icon">
|
||||
<v-icon size="64" color="white">mdi-ticket-account</v-icon>
|
||||
</div>
|
||||
<div class="header-content">
|
||||
<h1 class="main-title">Pilih Loket Antrean</h1>
|
||||
<h1 class="main-title">Pilih Layar Antrean Masuk</h1>
|
||||
<p class="subtitle">RSUD dr. Saiful Anwar Provinsi Jawa Timur</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedLokets.length > 0" class="lokets-grid">
|
||||
<div v-if="paginatedScreens.length > 0" class="screens-grid">
|
||||
<div
|
||||
v-for="loket in paginatedLokets"
|
||||
:key="loket.id"
|
||||
class="loket-card"
|
||||
@click="navigateToLoket(loket.id)"
|
||||
v-for="screen in paginatedScreens"
|
||||
:key="screen.id"
|
||||
class="screen-card"
|
||||
@click="navigateToScreen(screen.id)"
|
||||
>
|
||||
<div class="loket-card-header">
|
||||
<v-icon size="32" color="primary">mdi-counter</v-icon>
|
||||
<div class="loket-info">
|
||||
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
|
||||
<p class="loket-details">No. {{ numberToLetter(loket.no) }}</p>
|
||||
<div class="screen-card-header">
|
||||
<v-icon size="32" color="primary">mdi-monitor</v-icon>
|
||||
<div class="screen-info">
|
||||
<h3 class="screen-name">{{ screen.namaScreen }}</h3>
|
||||
<p class="screen-details">{{ screen.nomorScreen }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loket-preview">
|
||||
<div class="loket-service-count">
|
||||
<v-icon size="18" color="primary">mdi-hospital-building</v-icon>
|
||||
<span>{{ loket.pelayanan?.length || 0 }} Pelayanan</span>
|
||||
<div class="screen-preview">
|
||||
<div class="screen-loket-count">
|
||||
<v-icon size="18" color="primary">mdi-view-dashboard</v-icon>
|
||||
<span>{{ screen.loket?.length || 0 }} Loket</span>
|
||||
</div>
|
||||
<div class="loket-tags">
|
||||
<div class="screen-tags">
|
||||
<v-chip
|
||||
v-for="(pelayanan, idx) in (loket.pelayanan || []).slice(0, 3)"
|
||||
v-for="(loketId, idx) in (screen.loket || []).slice(0, 3)"
|
||||
:key="idx"
|
||||
size="small"
|
||||
class="ma-1 chip-preview"
|
||||
>
|
||||
{{ pelayanan }}
|
||||
{{ getLoketNameById(loketId) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="(loket.pelayanan || []).length > 3"
|
||||
v-if="(screen.loket || []).length > 3"
|
||||
size="small"
|
||||
class="ma-1 chip-more"
|
||||
>
|
||||
+{{ (loket.pelayanan || []).length - 3 }}
|
||||
+{{ (screen.loket || []).length - 3 }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loket-card-footer">
|
||||
<div class="screen-card-footer">
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
@@ -66,9 +66,9 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-counter-off</v-icon>
|
||||
<h3>Tidak Ada Loket Tersedia</h3>
|
||||
<p>Silakan tambah loket terlebih dahulu di halaman master</p>
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-monitor-off</v-icon>
|
||||
<h3>Tidak Ada Layar Tersedia</h3>
|
||||
<p>Silakan tambah layar terlebih dahulu di halaman master</p>
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
@@ -80,7 +80,7 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedLokets.length > 0 && totalPages > 1" class="pagination">
|
||||
<div v-if="paginatedScreens.length > 0 && totalPages > 1" class="pagination">
|
||||
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
|
||||
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
|
||||
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
|
||||
@@ -90,30 +90,27 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore';
|
||||
import { useLoketStore } from '@/stores/loketStore';
|
||||
import { useRoute } from '#app';
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
});
|
||||
|
||||
const masterStore = useMasterStore();
|
||||
const antreanMasukScreenStore = useAntreanMasukScreenStore();
|
||||
const loketStore = useLoketStore();
|
||||
const route = useRoute();
|
||||
|
||||
// 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));
|
||||
// Helper to get loket name by ID
|
||||
const getLoketNameById = (loketId) => {
|
||||
const loket = loketStore.getLoketById(loketId);
|
||||
return loket ? loket.namaLoket : `Loket ${loketId}`;
|
||||
};
|
||||
|
||||
// Get all lokets
|
||||
// Untuk saat ini, hanya menampilkan loket 1
|
||||
// Nanti setiap loket akan menampilkan data yang berbeda
|
||||
const allLokets = computed(() => {
|
||||
const allLoketsData = masterStore.loketData?.value || masterStore.loketData || [];
|
||||
// Filter hanya loket 1 untuk saat ini
|
||||
return allLoketsData.filter(loket => loket.id === 1 || loket.no === 1);
|
||||
// Get all screens from store
|
||||
const allScreens = computed(() => {
|
||||
return antreanMasukScreenStore.antreanMasukScreenItems || [];
|
||||
});
|
||||
|
||||
// Pagination
|
||||
@@ -123,11 +120,11 @@ const page = computed({
|
||||
set: (val) => navigateTo({ query: { ...route.query, page: val } }),
|
||||
});
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(allLokets.value.length / itemsPerPage)));
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(allScreens.value.length / itemsPerPage)));
|
||||
|
||||
const paginatedLokets = computed(() => {
|
||||
const paginatedScreens = computed(() => {
|
||||
const start = (page.value - 1) * itemsPerPage;
|
||||
return allLokets.value.slice(start, start + itemsPerPage);
|
||||
return allScreens.value.slice(start, start + itemsPerPage);
|
||||
});
|
||||
|
||||
const goPrev = () => {
|
||||
@@ -138,17 +135,17 @@ const goNext = () => {
|
||||
if (page.value < totalPages.value) page.value = page.value + 1;
|
||||
};
|
||||
|
||||
const navigateToLoket = (loketId) => {
|
||||
navigateTo(`/anjungan/antreanmasuk/${loketId}`);
|
||||
const navigateToScreen = (screenId) => {
|
||||
navigateTo(`/anjungan/antreanmasuk/${screenId}`);
|
||||
};
|
||||
|
||||
const navigateToSettings = () => {
|
||||
navigateTo('/setting/masterloket');
|
||||
navigateTo('/setting/screenantreanmasuk');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loket-selection-container {
|
||||
.screen-selection-container {
|
||||
background: var(--color-neutral-300);
|
||||
min-height: 100vh;
|
||||
padding: 40px;
|
||||
@@ -199,7 +196,7 @@ const navigateToSettings = () => {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.lokets-grid {
|
||||
.screens-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
@@ -207,7 +204,7 @@ const navigateToSettings = () => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loket-card {
|
||||
.screen-card {
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
@@ -226,7 +223,7 @@ const navigateToSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.loket-card-header {
|
||||
.screen-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -234,11 +231,11 @@ const navigateToSettings = () => {
|
||||
border-bottom: 2px solid var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.loket-info {
|
||||
.screen-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loket-name {
|
||||
.screen-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
@@ -246,18 +243,18 @@ const navigateToSettings = () => {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.loket-details {
|
||||
.screen-details {
|
||||
font-size: 14px;
|
||||
color: var(--color-neutral-600);
|
||||
margin: 4px 0 0 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loket-preview {
|
||||
.screen-preview {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loket-service-count {
|
||||
.screen-loket-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -267,7 +264,7 @@ const navigateToSettings = () => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loket-tags {
|
||||
.screen-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
@@ -287,7 +284,7 @@ const navigateToSettings = () => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loket-card-footer {
|
||||
.screen-card-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@@ -329,7 +326,7 @@ const navigateToSettings = () => {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loket-selection-container {
|
||||
.screen-selection-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -347,7 +344,7 @@ const navigateToSettings = () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lokets-grid {
|
||||
.screens-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@@ -915,6 +915,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -923,7 +924,6 @@ onUnmounted(() => {
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
padding: 4px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.hospital-name {
|
||||
|
||||
@@ -0,0 +1,789 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card>
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="header-icon">
|
||||
<v-icon size="32" color="white">mdi-ticket-account</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h2 class="page-title">Screen Antrean Masuk</h2>
|
||||
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Konfigurasi Layar Antrean Masuk</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
color="white"
|
||||
@click="openTambahDialog"
|
||||
elevation="0"
|
||||
class="add-btn"
|
||||
>
|
||||
<v-icon left size="20">mdi-plus-circle</v-icon>
|
||||
Tambah Screen
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="antreanMasukScreenItems"
|
||||
:items-per-page="10"
|
||||
class="elevation-0 data-table"
|
||||
>
|
||||
<template v-slot:item.no="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.loket="{ item }">
|
||||
<div class="loket-tags">
|
||||
<v-chip
|
||||
v-for="(loketId, idx) in item.loket.slice(0, 3)"
|
||||
:key="idx"
|
||||
size="small"
|
||||
class="ma-1 chip-secondary-outline"
|
||||
>
|
||||
{{ getLoketNameById(loketId) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="item.loket.length > 3"
|
||||
size="small"
|
||||
class="ma-1 chip-neutral"
|
||||
>
|
||||
+{{ item.loket.length - 3 }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.layarInformasi="{ item }">
|
||||
<v-btn
|
||||
size="small"
|
||||
@click="openPreviewDialog(item)"
|
||||
variant="flat"
|
||||
class="btn-preview mr-2"
|
||||
>
|
||||
<v-icon size="16" left>mdi-eye</v-icon>
|
||||
Preview
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
size="small"
|
||||
@click="openEditDialog(item)"
|
||||
variant="flat"
|
||||
class="btn-edit mr-2"
|
||||
>
|
||||
<v-icon size="16" left>mdi-cog</v-icon>
|
||||
Konfigurasi
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
@click="handleDelete(item)"
|
||||
variant="outlined"
|
||||
class="btn-delete"
|
||||
>
|
||||
<v-icon size="16" left>mdi-delete</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Dialog Tambah/Edit -->
|
||||
<v-dialog v-model="dialog" max-width="700px" persistent scrollable>
|
||||
<v-card class="dialog-card">
|
||||
<v-card-title class="dialog-header">
|
||||
<span class="headline-4">{{ isEdit ? 'Konfigurasi Screen' : 'Tambah Screen Baru' }}</span>
|
||||
<v-btn icon variant="text" @click="closeDialog" size="small" class="btn-close">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="dialog-content">
|
||||
<v-form ref="formRef">
|
||||
<!-- Informasi Screen -->
|
||||
<div class="field-group">
|
||||
<div class="group-label">Informasi Screen</div>
|
||||
|
||||
<v-text-field
|
||||
label="Nama Screen"
|
||||
v-model="formData.namaScreen"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[v => !!v || 'Nama screen harus diisi']"
|
||||
hide-details="auto"
|
||||
class="mb-3 input-field"
|
||||
placeholder="Layar Antrean Masuk 1"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Nomor Screen"
|
||||
v-model="formData.nomorScreen"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
class="mb-3 input-field"
|
||||
placeholder="AM-001"
|
||||
></v-text-field>
|
||||
<small class="caption-2">Nomor identifikasi unik untuk screen</small>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4 divider-section"></v-divider>
|
||||
|
||||
<!-- Loket Selection -->
|
||||
<div class="field-group">
|
||||
<div class="group-label">
|
||||
<v-icon size="18" class="icon-label">mdi-view-dashboard</v-icon>
|
||||
<span>Loket yang Ditampilkan</span>
|
||||
</div>
|
||||
|
||||
<v-select
|
||||
label="Pilih Loket"
|
||||
:items="loketOptions"
|
||||
v-model="formData.loket"
|
||||
item-title="nama"
|
||||
item-value="id"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
:rules="[v => v.length > 0 || 'Pilih minimal 1 loket']"
|
||||
hide-details="auto"
|
||||
class="input-field"
|
||||
>
|
||||
<template v-slot:chip="{ props, item }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
closable
|
||||
size="small"
|
||||
class="chip-secondary"
|
||||
>
|
||||
{{ item.raw.nama }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<v-chip size="x-small" class="chip-secondary-small">{{ item.raw.nama }}</v-chip>
|
||||
</template>
|
||||
<v-list-item-title class="body-3">{{ item.raw.nama }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Selected Preview -->
|
||||
<div v-if="formData.loket.length > 0" class="selected-preview">
|
||||
<v-icon size="14" class="icon-success">mdi-check-circle</v-icon>
|
||||
<small class="caption-2">{{ formData.loket.length }} loket dipilih</small>
|
||||
</div>
|
||||
|
||||
<!-- Grid Preview -->
|
||||
<div v-if="formData.loket.length > 0" class="loket-grid mt-4">
|
||||
<div class="grid-label">Preview Tampilan:</div>
|
||||
<div class="loket-preview-grid">
|
||||
<v-chip
|
||||
v-for="loketId in formData.loket"
|
||||
:key="loketId"
|
||||
size="small"
|
||||
class="ma-1 chip-secondary-outline"
|
||||
>
|
||||
{{ getLoketNameById(loketId) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="dialog-actions">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="closeDialog"
|
||||
class="btn-cancel"
|
||||
>
|
||||
<v-icon left size="18">mdi-close</v-icon>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
@click="submitForm"
|
||||
class="btn-submit"
|
||||
>
|
||||
<v-icon left size="18">mdi-content-save</v-icon>
|
||||
Simpan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Preview Dialog -->
|
||||
<v-dialog v-model="previewDialog" max-width="95vw" max-height="95vh" persistent>
|
||||
<v-card class="preview-dialog-card">
|
||||
<v-card-title class="preview-dialog-header">
|
||||
<div class="preview-header-content">
|
||||
<v-icon size="24" class="mr-2">mdi-ticket-account</v-icon>
|
||||
<span class="headline-4">Preview Screen: {{ previewItem?.namaScreen || '' }}</span>
|
||||
</div>
|
||||
<v-btn icon variant="text" size="small" class="btn-close" @click="closePreviewDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text class="preview-content">
|
||||
<iframe
|
||||
v-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
class="preview-iframe"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<div v-else class="preview-loading">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="mt-4">Memuat preview...</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
<span class="body-3">{{ snackbar.message }}</span>
|
||||
<template v-slot:actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore';
|
||||
import { useLoketStore } from '@/stores/loketStore';
|
||||
|
||||
const antreanMasukScreenStore = useAntreanMasukScreenStore();
|
||||
const loketStore = useLoketStore();
|
||||
const dialog = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref(null);
|
||||
const previewDialog = ref(false);
|
||||
const previewItem = ref(null);
|
||||
const previewUrl = ref('');
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
});
|
||||
|
||||
const headers = ref([
|
||||
{ title: "No", value: "no", sortable: false, width: "80px" },
|
||||
{ title: "Nama Screen", value: "namaScreen", sortable: true },
|
||||
{ title: "Nomor Screen", value: "nomorScreen", sortable: true },
|
||||
{ title: "Loket Ditampilkan", value: "loket", sortable: false },
|
||||
{ title: "Layar Informasi", value: "layarInformasi", sortable: false, width: "150px" },
|
||||
{ title: "Actions", value: "actions", sortable: false, width: "auto" },
|
||||
]);
|
||||
|
||||
const antreanMasukScreenItems = computed(() => antreanMasukScreenStore.antreanMasukScreenItems);
|
||||
|
||||
// Get loket options for select dropdown
|
||||
const loketOptions = computed(() => {
|
||||
// Directly access loketStore.loketData.value since it's a ref
|
||||
// This is the most reliable way to get all lokets
|
||||
const loketsArray = loketStore.loketData?.value || loketStore.loketData || [];
|
||||
|
||||
// Ensure it's an array
|
||||
if (!Array.isArray(loketsArray)) {
|
||||
console.warn('loketData is not an array:', loketsArray);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Debug: log jumlah loket yang ditemukan
|
||||
if (loketsArray.length > 0) {
|
||||
console.log(`[ScreenAntreanMasuk] Found ${loketsArray.length} lokets:`, loketsArray.map(l => l.namaLoket || l.nama));
|
||||
} else {
|
||||
console.warn('[ScreenAntreanMasuk] No lokets found in loketStore');
|
||||
}
|
||||
|
||||
// Map ALL lokets to options format - return everything, no filtering or limiting
|
||||
return loketsArray.map(loket => ({
|
||||
id: loket.id,
|
||||
nama: loket.namaLoket || `Loket ${loket.no || loket.id}`
|
||||
}));
|
||||
});
|
||||
|
||||
// Helper to get loket name by ID
|
||||
const getLoketNameById = (loketId) => {
|
||||
// Get loket directly from loketStore
|
||||
const loketsArray = loketStore.loketData?.value || loketStore.loketData || [];
|
||||
const loket = loketsArray.find(l => l.id === loketId);
|
||||
return loket ? loket.namaLoket : `Loket ${loketId}`;
|
||||
};
|
||||
|
||||
const formData = ref({
|
||||
id: null,
|
||||
namaScreen: '',
|
||||
nomorScreen: '',
|
||||
loket: [],
|
||||
});
|
||||
|
||||
const openTambahDialog = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const openEditDialog = (item) => {
|
||||
isEdit.value = true;
|
||||
formData.value = {
|
||||
id: item.id,
|
||||
namaScreen: item.namaScreen,
|
||||
nomorScreen: item.nomorScreen,
|
||||
loket: [...item.loket],
|
||||
};
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialog.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: null,
|
||||
namaScreen: '',
|
||||
nomorScreen: '',
|
||||
loket: [],
|
||||
};
|
||||
if (formRef.value) {
|
||||
formRef.value.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
const { valid } = await formRef.value.validate();
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
if (isEdit.value) {
|
||||
// Update screen
|
||||
const result = antreanMasukScreenStore.updateAntreanMasukScreen(formData.value);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: result.message,
|
||||
color: result.success ? 'success' : 'error'
|
||||
};
|
||||
} else {
|
||||
// Add new screen
|
||||
const result = antreanMasukScreenStore.addAntreanMasukScreen(formData.value);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: result.message,
|
||||
color: result.success ? 'success' : 'error'
|
||||
};
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
if (confirm(`Hapus screen ${item.namaScreen}?`)) {
|
||||
const result = antreanMasukScreenStore.deleteAntreanMasukScreen(item.id);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: result.message,
|
||||
color: result.success ? 'success' : 'error'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const openPreviewDialog = (item) => {
|
||||
previewItem.value = item;
|
||||
// Generate preview URL untuk screen dengan ID
|
||||
// Note: Preview akan menampilkan halaman antrean masuk dengan ID screen
|
||||
previewUrl.value = `/anjungan/antreanmasuk/${item.id}`;
|
||||
previewDialog.value = true;
|
||||
};
|
||||
|
||||
const closePreviewDialog = () => {
|
||||
previewDialog.value = false;
|
||||
previewItem.value = null;
|
||||
previewUrl.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* Colors from Design System */
|
||||
$neutral-900: #212121;
|
||||
$neutral-800: #4D4D4D;
|
||||
$neutral-700: #717171;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-500: #ABBED1;
|
||||
$neutral-400: #E5F7FA;
|
||||
$neutral-300: #F5F7FA;
|
||||
$neutral-100: #FFFFFF;
|
||||
|
||||
$secondary-700: #0053AD;
|
||||
$secondary-600: #0671E0;
|
||||
$secondary-300: #DBEDFF;
|
||||
|
||||
$success-600: #009262;
|
||||
$success-300: #84DFC1;
|
||||
$success-200: #F1FBF8;
|
||||
|
||||
$danger-600: #E02B1D;
|
||||
|
||||
// Font Family & Weights
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
|
||||
// Apply font family
|
||||
* {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PAGE HEADER
|
||||
// ============================================
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 4px 16px rgba(6, 113, 224, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px;
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-right: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin: 0;
|
||||
color: $neutral-100;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 4px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
font-weight: $font-weight-semibold;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: $secondary-600 !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATA TABLE
|
||||
// ============================================
|
||||
.data-table {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.loket-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.chip-secondary-outline {
|
||||
border: 1px solid $secondary-600;
|
||||
background-color: transparent !important;
|
||||
color: $secondary-600 !important;
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.chip-neutral {
|
||||
background-color: $neutral-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: $secondary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
border-color: $danger-600 !important;
|
||||
color: $danger-600 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DIALOG
|
||||
// ============================================
|
||||
.dialog-card {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
|
||||
color: $neutral-100;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headline-4 {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
color: $neutral-100 !important;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 24px !important;
|
||||
background: $neutral-300;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 16px 24px;
|
||||
background: $neutral-300;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FORM ELEMENTS
|
||||
// ============================================
|
||||
.field-group {
|
||||
background: $neutral-100;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0;
|
||||
border: 1px solid $neutral-400;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $secondary-600;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
color: $secondary-600 !important;
|
||||
}
|
||||
|
||||
.caption-2 {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $neutral-700;
|
||||
}
|
||||
|
||||
.body-3 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: $font-weight-regular;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.divider-section {
|
||||
border-color: $neutral-400 !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CHIPS
|
||||
// ============================================
|
||||
.chip-secondary {
|
||||
background-color: $secondary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.chip-secondary-small {
|
||||
background-color: $secondary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SELECTED PREVIEW
|
||||
// ============================================
|
||||
.selected-preview {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: $success-200;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid $success-300;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
color: $success-600 !important;
|
||||
}
|
||||
|
||||
.loket-grid {
|
||||
background: $neutral-300;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $neutral-400;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $neutral-700;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.loket-preview-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUTTONS
|
||||
// ============================================
|
||||
.btn-cancel {
|
||||
border-color: $neutral-600 !important;
|
||||
color: $neutral-800 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: $secondary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PREVIEW DIALOG
|
||||
// ============================================
|
||||
.btn-preview {
|
||||
background-color: $success-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.preview-dialog-card {
|
||||
font-family: $font-family-base;
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-dialog-header {
|
||||
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
|
||||
color: $neutral-100;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 0 !important;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $neutral-800;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 80vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $neutral-100;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
// stores/antreanMasukScreenStore.js
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useAntreanMasukScreenStore = defineStore('antreanMasukScreen', () => {
|
||||
// Initial antrean masuk screen items data
|
||||
const antreanMasukScreenItems = ref([
|
||||
{
|
||||
id: 1,
|
||||
namaScreen: "Layar Antrean Masuk 1",
|
||||
nomorScreen: "AM-001",
|
||||
loket: [1, 2], // Array of loket IDs
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
namaScreen: "Layar Antrean Masuk 2",
|
||||
nomorScreen: "AM-002",
|
||||
loket: [3, 4],
|
||||
},
|
||||
]);
|
||||
|
||||
// Computed
|
||||
const getAntreanMasukScreenById = (id) => {
|
||||
return computed(() => {
|
||||
const targetId = Number(id);
|
||||
return antreanMasukScreenItems.value.find((s) => Number(s.id) === targetId);
|
||||
});
|
||||
};
|
||||
|
||||
const getAllAntreanMasukScreens = computed(() => antreanMasukScreenItems.value);
|
||||
|
||||
// Actions
|
||||
const addAntreanMasukScreen = (screenPayload) => {
|
||||
// Ensure we get a valid ID even if antreanMasukScreenItems is empty
|
||||
const maxId = antreanMasukScreenItems.value.length > 0
|
||||
? Math.max(...antreanMasukScreenItems.value.map(s => s.id), 0)
|
||||
: 0;
|
||||
const newId = maxId + 1;
|
||||
// Pastikan id baru tidak tertimpa payload (payload.id bisa null)
|
||||
const newScreen = {
|
||||
...screenPayload,
|
||||
id: newId,
|
||||
};
|
||||
antreanMasukScreenItems.value.push(newScreen);
|
||||
return { success: true, message: `Screen ${newScreen.namaScreen} berhasil ditambahkan`, data: newScreen };
|
||||
};
|
||||
|
||||
const updateAntreanMasukScreen = (screenPayload) => {
|
||||
const index = antreanMasukScreenItems.value.findIndex(s => s.id === screenPayload.id);
|
||||
if (index !== -1) {
|
||||
antreanMasukScreenItems.value[index] = {
|
||||
...antreanMasukScreenItems.value[index],
|
||||
...screenPayload,
|
||||
};
|
||||
return { success: true, message: `Konfigurasi ${screenPayload.namaScreen} berhasil disimpan` };
|
||||
}
|
||||
return { success: false, message: 'Screen tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deleteAntreanMasukScreen = (screenId) => {
|
||||
const index = antreanMasukScreenItems.value.findIndex(s => s.id === screenId);
|
||||
if (index !== -1) {
|
||||
const screenName = antreanMasukScreenItems.value[index].namaScreen;
|
||||
antreanMasukScreenItems.value.splice(index, 1);
|
||||
return { success: true, message: `Screen ${screenName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Screen tidak ditemukan' };
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
antreanMasukScreenItems,
|
||||
|
||||
// Computed
|
||||
getAllAntreanMasukScreens,
|
||||
getAntreanMasukScreenById,
|
||||
|
||||
// Actions
|
||||
addAntreanMasukScreen,
|
||||
updateAntreanMasukScreen,
|
||||
deleteAntreanMasukScreen,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'antrean-masuk-screen-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['antreanMasukScreenItems'],
|
||||
},
|
||||
});
|
||||
+101
-3
@@ -43,9 +43,28 @@ const getPelayananContohDariAnjungan = (anjunganItems) => {
|
||||
loket4: klinikAnjunganReguler.filter(k => ['KK', 'PR'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['KK', 'PR'].includes(k))
|
||||
: klinikAnjunganReguler.slice(7, 9), // Ambil 2 berikutnya
|
||||
loket5: klinikAnjunganReguler.filter(k => ['BD', 'GI'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['BD', 'GI'].includes(k))
|
||||
: klinikAnjunganReguler.slice(9, 11), // Ambil 2 berikutnya
|
||||
loket6: klinikAnjunganReguler.filter(k => ['GR', 'GZ'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['GR', 'GZ'].includes(k))
|
||||
: klinikAnjunganReguler.slice(11, 13), // Ambil 2 berikutnya
|
||||
loket7: klinikAnjunganReguler.filter(k => ['IP', 'MT'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['IP', 'MT'].includes(k))
|
||||
: klinikAnjunganReguler.slice(13, 15), // Ambil 2 berikutnya
|
||||
loket8: klinikAnjunganReguler.filter(k => ['OB', 'HO'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['OB', 'HO'].includes(k))
|
||||
: klinikAnjunganReguler.slice(15, 17), // Ambil 2 berikutnya
|
||||
loket9: klinikAnjunganReguler.filter(k => ['AN', 'BD'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['AN', 'BD'].includes(k))
|
||||
: klinikAnjunganReguler.slice(0, 2), // Ambil 2 pertama jika tidak ada match
|
||||
loket10: klinikAnjunganReguler.filter(k => ['GI', 'GZ'].includes(k)).length > 0
|
||||
? klinikAnjunganReguler.filter(k => ['GI', 'GZ'].includes(k))
|
||||
: klinikAnjunganReguler.slice(2, 4), // Ambil 2 berikutnya
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const useLoketStore = defineStore('loket', () => {
|
||||
const clinicStore = useClinicStore();
|
||||
const anjunganStore = useAnjunganStore();
|
||||
@@ -58,7 +77,7 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
try {
|
||||
const anjunganItems = anjunganStore.getAllAnjungan || [];
|
||||
return getPelayananContohDariAnjungan(anjunganItems);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Fallback jika store belum terinisialisasi
|
||||
console.warn('AnjunganStore belum terinisialisasi, menggunakan data default');
|
||||
return getPelayananContohDariAnjungan([]);
|
||||
@@ -69,7 +88,9 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
// Data akan diupdate saat store diinisialisasi
|
||||
const initLoketData = () => {
|
||||
const pelayananContoh = getPelayananContoh();
|
||||
return [
|
||||
|
||||
// Base loket (A-J) dengan pelayanan dari contoh
|
||||
const baseLoket = [
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
@@ -110,12 +131,83 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
no: 5,
|
||||
namaLoket: "Loket E",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket5, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
no: 6,
|
||||
namaLoket: "Loket F",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket6, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
no: 7,
|
||||
namaLoket: "Loket G",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket7, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
no: 8,
|
||||
namaLoket: "Loket H",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket8, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
no: 9,
|
||||
namaLoket: "Loket I",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket9, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
no: 10,
|
||||
namaLoket: "Loket J",
|
||||
kuota: 500,
|
||||
pelayanan: pelayananContoh.loket10, // Dari master anjungan
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
];
|
||||
|
||||
return baseLoket;
|
||||
};
|
||||
|
||||
// State - Loket Data
|
||||
// Data pelayanan diambil dari contoh master anjungan
|
||||
const loketData = ref(initLoketData());
|
||||
const initialData = initLoketData();
|
||||
// Initialize with default data, will be overridden by persisted data if exists
|
||||
const loketData = ref(initialData);
|
||||
|
||||
// After store is created, check if data needs migration
|
||||
// This runs after Pinia restores from localStorage
|
||||
// If loketData has less than 10 items, reinitialize with default data (A-J)
|
||||
if (loketData.value && loketData.value.length < 10) {
|
||||
loketData.value = initialData;
|
||||
}
|
||||
|
||||
// Computed - Available services (reference dari clinicStore)
|
||||
// Mengambil data dari clinicStore untuk dropdown pelayanan
|
||||
@@ -246,6 +338,12 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
if (value && value.loketData && !Array.isArray(value.loketData)) {
|
||||
value.loketData = [];
|
||||
}
|
||||
// If restored data has less than 10 lokets, reinitialize with default data (A-J)
|
||||
if (value && value.loketData && Array.isArray(value.loketData) && value.loketData.length < 10) {
|
||||
// Reinitialize with default data (A-J) - this will happen on next store initialization
|
||||
// For now, we'll let the store initialization handle it
|
||||
value.loketData = null; // Force reinitialization
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,6 +57,7 @@ const defaultNavItems: NavItem[] = [
|
||||
{ id: 21, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
|
||||
{ id: 22, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
|
||||
{ id: 23, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
|
||||
{ id: 24, name: "Screen Antrean Masuk", path: "/setting/screenantreanmasuk", icon: "mdi-circle-small" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user