update sidebar checkin dan antrian loket

This commit is contained in:
Fanrouver
2026-01-26 09:46:38 +07:00
parent 75a96389fe
commit 525322b6f6
8 changed files with 892 additions and 349 deletions
+4 -4
View File
@@ -35,17 +35,17 @@ KEYCLOAK_ISSUER="https://auth.rssa.top/realms/sandbox"
#AUTH ORIGIN UNTUK ACCESS APLIKASI login dari KEYCLOAK
# AUTH_ORIGIN="http://10.10.150.175:3000"
AUTH_ORIGIN="0.0.0.0"
# AUTH_ORIGIN="0.0.0.0"
# AUTH_ORIGIN="http://localhost:3000"
# AUTH_ORIGIN="https://antrean.dev.rssa.id"
AUTH_ORIGIN="https://antrean.dev.rssa.id"
# AUTH_ORIGIN="https://antrean.rssa.id"
#HOST UNTUK ACCESS APLIKASI
# HOST="http://localhost:3000"
# HOST="http://10.10.150.175:3000"
# HOST="https://antrean.rssa.id"
HOST="https://antrean.dev.rssa.id"
# HOST=
# HOST="https://antrean.dev.rssa.id"
HOST="0.0.0.0"
# HOST=
# Custom Keycloak logout endpoint UBAH SESUAI REALM DAN DOMAIN KEYCLOAK ANDA
+332
View File
@@ -0,0 +1,332 @@
# Dashboard Dummy Data Documentation
Dokumentasi lengkap data dummy yang digunakan di `pages/Dashboard.vue`
## 📊 Statistics Cards (Header Mini Stats)
Data statistik mini yang ditampilkan di header:
```javascript
{
pasienHariIni: 324,
antreanAktif: 47
}
```
## 📈 Main Statistics Cards
Data untuk 4 kartu statistik utama:
### 1. Pasien Hari Ini
```javascript
{
label: 'Pasien Hari Ini',
value: '324',
icon: 'mdi-account-heart',
iconColor: 'white',
color: 'primary',
change: '+18.2%',
changeType: 'positive',
changeIcon: 'mdi-trending-up'
}
```
### 2. Antrean Aktif
```javascript
{
label: 'Antrean Aktif',
value: '47',
icon: 'mdi-clock-fast',
iconColor: 'white',
color: 'secondary',
change: '+5',
changeType: 'positive',
changeIcon: 'mdi-arrow-up'
}
```
### 3. Rata-rata Tunggu
```javascript
{
label: 'Rata-rata Tunggu',
value: '12 min',
icon: 'mdi-timer-outline',
iconColor: 'white',
color: 'success',
change: '-3 min',
changeType: 'positive',
changeIcon: 'mdi-trending-down'
}
```
### 4. Tingkat Kehadiran
```javascript
{
label: 'Tingkat Kehadiran',
value: '89.5%',
icon: 'mdi-check-circle',
iconColor: 'white',
color: 'primary',
change: '+2.3%',
changeType: 'positive',
changeIcon: 'mdi-trending-up'
}
```
---
## 📊 Chart 1: Tren Kunjungan Pasien (Line/Area Chart)
Chart yang menampilkan tren kunjungan pasien bulanan untuk tahun 2025.
### Labels (Bulan)
```javascript
[
"Jan",
"Feb",
"Mar",
"Apr",
"Mei",
"Jun",
"Jul",
"Ags",
"Sep",
"Okt",
"Nov",
"Des",
];
```
### Dataset 1: Pasien Umum
```javascript
{
label: 'Pasien Umum',
data: [850, 920, 880, 1050, 1120, 1080, 1200, 1150, 1080, 1190, 1250, 1300],
backgroundColor: 'rgba(86, 126, 231, 0.2)',
borderColor: '#567EE7' // primary-500
}
```
**Total Pasien Umum 2025**: 13,070 pasien
### Dataset 2: Pasien BPJS
```javascript
{
label: 'Pasien BPJS',
data: [1200, 1350, 1280, 1480, 1550, 1620, 1700, 1650, 1590, 1720, 1800, 1850],
backgroundColor: 'rgba(255, 132, 65, 0.2)',
borderColor: '#FF8441' // secondary-500
}
```
**Total Pasien BPJS 2025**: 18,790 pasien
**Grand Total**: 31,860 pasien sepanjang tahun 2025
---
## 💰 Chart 2: Status Pembayaran (Doughnut Chart)
Distribusi metode pembayaran pasien.
### Data
```javascript
{
labels: ['BPJS Kesehatan', 'Umum/Tunai', 'Asuransi Swasta', 'Corporate'],
datasets: [{
data: [1850, 680, 320, 280]
}]
}
```
### Breakdown Persentase
| Metode Pembayaran | Jumlah | Persentase |
| ----------------- | --------- | ---------- |
| BPJS Kesehatan | 1,850 | 58.6% |
| Umum/Tunai | 680 | 21.5% |
| Asuransi Swasta | 320 | 10.1% |
| Corporate | 280 | 8.9% |
| **Total** | **3,130** | **100%** |
---
## ⏱️ Chart 3: Waktu Tunggu per Poli (Horizontal Bar Chart)
Rata-rata waktu tunggu untuk setiap poliklinik (dalam menit).
### Data
```javascript
{
labels: ['Poli Umum', 'Poli Anak', 'Poli Gigi', 'Poli Mata', 'Poli THT', 'Poli Jantung'],
datasets: [{
label: 'Waktu Tunggu (menit)',
data: [15, 12, 8, 18, 10, 22]
}]
}
```
### Ranking Waktu Tunggu
| Rank | Poliklinik | Waktu Tunggu |
| ---- | ------------ | ------------ |
| 1 | Poli Gigi | 8 menit ⚡ |
| 2 | Poli THT | 10 menit |
| 3 | Poli Anak | 12 menit |
| 4 | Poli Umum | 15 menit |
| 5 | Poli Mata | 18 menit |
| 6 | Poli Jantung | 22 menit 🐌 |
**Rata-rata Total**: 14.17 menit
---
## ✅ Chart 4: Tingkat Kehadiran (Polar Area Chart)
Status kehadiran pasien dalam seminggu.
### Data
```javascript
{
labels: ['Hadir Tepat Waktu', 'Hadir Terlambat', 'Tidak Hadir', 'Batal', 'Reschedule'],
datasets: [{
data: [1850, 420, 280, 150, 230]
}]
}
```
### Breakdown Persentase
| Status | Jumlah | Persentase |
| -------------------- | --------- | ---------- |
| Hadir Tepat Waktu ✅ | 1,850 | 62.6% |
| Hadir Terlambat ⏰ | 420 | 14.2% |
| Tidak Hadir ❌ | 280 | 9.5% |
| Batal 🚫 | 150 | 5.1% |
| Reschedule 📅 | 230 | 7.8% |
| **Total** | **2,960** | **100%** |
**Tingkat Kehadiran Total** (Hadir Tepat Waktu + Hadir Terlambat): 76.8%
---
## 🎨 Color Palette
Palet warna yang digunakan dalam dashboard:
### Primary Colors
```javascript
{
700: '#3556AE',
600: '#3A61C9',
500: '#567EE7',
400: '#7898EC',
300: '#9AB2F1',
200: '#BBCBF5'
}
```
### Secondary Colors
```javascript
{
700: '#E65A0D',
600: '#F16F29',
500: '#FF8441',
400: '#FF9D67',
300: '#FFB58D'
}
```
### Success Colors
```javascript
{
700: '#008D65',
600: '#33A484',
500: '#59B59B',
400: '#80C6B2'
}
```
### Neutral Colors
```javascript
{
100: '#FFFFFF',
300: '#F5F7FA',
500: '#CDD4DC',
700: '#717171',
900: '#212121'
}
```
---
## 📤 Export Options
Menu export data yang tersedia:
```javascript
[
{ title: "JSON (.json)", type: "json", icon: "mdi-code-json" },
{ title: "CSV (.csv)", type: "csv", icon: "mdi-file-delimited" },
{
title: "Excel (.xlsx) - Server Only",
type: "excel",
icon: "mdi-file-excel",
},
{ title: "PDF (.pdf) - Placeholder", type: "pdf", icon: "mdi-file-pdf" },
];
```
---
## 📝 Summary Insights
### Total Visitors
- **Hari Ini**: 324 pasien (+18.2%)
- **Bulan Ini** (estimasi): ~2,617 pasien
- **Tahun 2025** (dari chart): 31,860 pasien
### Performance Metrics
- **Antrean Aktif**: 47 (+5)
- **Rata-rata Waktu Tunggu**: 12 menit (-3 min improvement)
- **Tingkat Kehadiran**: 89.5% (+2.3%)
- **Kehadiran Tepat Waktu**: 62.6%
### Top Performing Clinic
- **Tercepat**: Poli Gigi (8 menit)
- **Terlambat**: Poli Jantung (22 menit)
### Payment Distribution
- **BPJS dominan**: 58.6% dari total transaksi
- **Non-BPJS**: 41.4% (Umum + Asuransi + Corporate)
---
## 🔧 Usage Notes
File ini mendokumentasikan semua data dummy yang digunakan dalam komponen Dashboard.vue. Data ini dapat digunakan sebagai:
1. **Reference** untuk struktur data API
2. **Testing data** untuk pengembangan
3. **Template** untuk implementasi data real dari backend
4. **Documentation** untuk tim pengembang
Semua data bersifat **dummy/sample** dan harus diganti dengan data real dari API/backend saat production.
+1 -3
View File
@@ -58,8 +58,6 @@
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
</td>
</tr>
@@ -245,7 +243,7 @@ const {
padding-bottom: 0;
margin-bottom: 6mm;
border-top: 2px solid #000;
font-size: 9px;
font-size: 10px;
font-weight: bold;
text-align: center;
line-height: 1.0;
+148 -120
View File
@@ -46,7 +46,6 @@
>
<template #activator="{ props: menuProps }">
<v-tooltip
:disabled="!rail"
open-on-hover
location="end"
:text="item.name"
@@ -63,17 +62,26 @@
</v-tooltip>
</template>
<v-list density="compact" class="sidebar-popup-menu">
<v-list-item
<v-tooltip
v-for="child in item.children"
:key="child.name"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="sidebar-nav-item"
active-class="sidebar-nav-item-active"
@click="closePopup"
></v-list-item>
open-on-hover
location="end"
:text="child.name"
>
<template #activator="{ props: tooltipProps }">
<v-list-item
v-bind="tooltipProps"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="sidebar-nav-item"
active-class="sidebar-nav-item-active"
@click="closePopup"
></v-list-item>
</template>
</v-tooltip>
</v-list>
</v-menu>
@@ -93,21 +101,29 @@
></v-list-item>
</template>
<v-list-item
<v-tooltip
v-for="child in item.children"
:key="child.name"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="pl-8 sidebar-nav-item"
active-class="sidebar-nav-item-active"
></v-list-item>
open-on-hover
location="end"
:text="child.name"
>
<template #activator="{ props: tooltipProps }">
<v-list-item
v-bind="tooltipProps"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="pl-8 sidebar-nav-item"
active-class="sidebar-nav-item-active"
></v-list-item>
</template>
</v-tooltip>
</v-list-group>
<v-tooltip
v-else
:disabled="!rail"
open-on-hover
location="end"
:text="item.name"
@@ -129,104 +145,106 @@
<v-divider></v-divider>
<v-list v-if="user" nav density="compact" class="pa-2 flex-shrink-0">
<ProfilePopup :user="user" @logout="handleLogout" :rail="rail" />
</v-list>
<v-list v-else nav density="compact" class="flex-shrink-0">
<v-list-item
@click="redirectToLogin"
prepend-icon="mdi-login"
title="Login"
link
class="sidebar-login-item"
></v-list-item>
</v-list>
<!-- Bottom section wrapper for profile and lock button -->
<div class="sidebar-bottom-section">
<v-list v-if="user" nav density="compact" class="pa-2">
<ProfilePopup :user="user" @logout="handleLogout" :rail="rail" />
</v-list>
<v-list v-else nav density="compact">
<v-list-item
@click="redirectToLogin"
prepend-icon="mdi-login"
title="Login"
link
class="sidebar-login-item"
></v-list-item>
</v-list>
<!-- Lock Sidebar Toggle - Positioned at bottom left -->
<div class="sidebar-lock-container">
<v-tooltip
:disabled="!rail"
open-on-hover
location="end"
:text="isLocked ? 'Buka Sidebar' : 'Kunci Sidebar'"
>
<template #activator="{ props: tooltipProps }">
<v-btn
v-bind="tooltipProps"
:class="['sidebar-lock-btn', { 'sidebar-lock-btn-locked': isLocked }]"
variant="text"
size="small"
@click="handleLockToggle(!isLocked)"
>
<!-- Icon Collapsed Sidebar (sidebar collapsed/locked) -->
<svg
v-if="isLocked"
class="sidebar-toggle-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<!-- Lock Sidebar Toggle -->
<div class="sidebar-lock-container">
<v-tooltip
open-on-hover
location="end"
:text="isLocked ? 'Buka Sidebar' : 'Kunci Sidebar'"
>
<template #activator="{ props: tooltipProps }">
<v-btn
v-bind="tooltipProps"
:class="['sidebar-lock-btn', { 'sidebar-lock-btn-locked': isLocked }]"
variant="text"
size="small"
@click="handleLockToggle(!isLocked)"
>
<!-- Rounded rectangle container -->
<rect
x="2"
y="2"
width="16"
height="16"
rx="3"
fill="var(--color-primary-100)"
/>
<!-- Sidebar area (kiri, narrow ~30%, biru gelap) -->
<rect
x="2"
y="2"
width="6"
height="16"
rx="3"
fill="var(--color-primary-600)"
/>
</svg>
<!-- Icon Expanded Sidebar (sidebar expanded/unlocked) -->
<svg
v-else
class="sidebar-toggle-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Rounded rectangle container -->
<rect
x="2"
y="2"
width="16"
height="16"
rx="3"
fill="var(--color-primary-100)"
/>
<!-- Sidebar area (kiri, wider ~55%, biru gelap) -->
<rect
x="2"
y="2"
width="11"
height="16"
rx="3"
fill="var(--color-primary-600)"
/>
<!-- Left-pointing arrow in the white area -->
<path
d="M 15.5 10 L 13.5 8 M 15.5 10 L 13.5 12"
stroke="var(--color-primary-600)"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</v-btn>
</template>
</v-tooltip>
<!-- Icon Collapsed Sidebar (sidebar collapsed/locked) -->
<svg
v-if="isLocked"
class="sidebar-toggle-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Rounded rectangle container -->
<rect
x="2"
y="2"
width="16"
height="16"
rx="3"
fill="var(--color-primary-100)"
/>
<!-- Sidebar area (kiri, narrow ~30%, biru gelap) -->
<rect
x="2"
y="2"
width="6"
height="16"
rx="3"
fill="var(--color-primary-600)"
/>
</svg>
<!-- Icon Expanded Sidebar (sidebar expanded/unlocked) -->
<svg
v-else
class="sidebar-toggle-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Rounded rectangle container -->
<rect
x="2"
y="2"
width="16"
height="16"
rx="3"
fill="var(--color-primary-100)"
/>
<!-- Sidebar area (kiri, wider ~55%, biru gelap) -->
<rect
x="2"
y="2"
width="11"
height="16"
rx="3"
fill="var(--color-primary-600)"
/>
<!-- Left-pointing arrow in the white area -->
<path
d="M 15.5 10 L 13.5 8 M 15.5 10 L 13.5 12"
stroke="var(--color-primary-600)"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</v-btn>
</template>
</v-tooltip>
</div>
</div>
</v-navigation-drawer>
</template>
@@ -329,7 +347,17 @@ onMounted(async () => {
@use '~/assets/scss/_colors.scss' as *;
:deep(.v-navigation-drawer) {
position: relative;
position: fixed !important;
top: 0 !important;
left: 0 !important;
height: 100vh !important;
z-index: 1000 !important;
}
.sidebar-bottom-section {
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-logo-container {
@@ -472,10 +500,10 @@ onMounted(async () => {
}
.sidebar-lock-container {
position: absolute;
bottom: 16px;
left: 8px;
z-index: 10;
padding: 8px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.sidebar-lock-btn {
+1 -3
View File
@@ -329,7 +329,7 @@ export const useThermalPrint = () => {
padding-bottom: 0;
margin-bottom: 6mm;
border-top: 2px solid #000;
font-size: 9px;
font-size: 10px;
font-weight: bold;
text-align: center;
line-height: 1.0;
@@ -462,8 +462,6 @@ export const useThermalPrint = () => {
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
<!-- Feed lines untuk auto cutter (space setelah footer untuk cutter) -->
+1 -1
View File
@@ -12,7 +12,7 @@
<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>
</div>
<div class="loket-selection-container">
+404 -217
View File
@@ -15,10 +15,10 @@
<!-- Main Content Grid - Same pattern as AdminLoket.vue -->
<v-row class="content-grid" dense>
<!-- Left Column: QR Scan Card -->
<v-col cols="12" md="6" lg="5" class="sticky-column">
<div class="sticky-wrapper">
<v-card class="qr-scan-card" elevation="0">
<v-card-text class="pa-4">
<v-col cols="12" md="6" lg="5" class="sticky-column d-flex">
<div class="sticky-wrapper" style="width: 100%;">
<v-card class="qr-scan-card" elevation="0" style="height: 100%;">
<v-card-text class="pa-4 d-flex flex-column" style="height: 100%;">
<!-- Status Header -->
<div class="status-header mb-3">
<div class="status-icon-wrapper">
@@ -50,7 +50,8 @@
v-else-if="!hasCamera && !isScanning"
type="warning"
variant="tonal"
class="mb-4"
class="mb-3 compact-alert"
density="compact"
>
<template v-slot:prepend>
<v-icon>mdi-camera-off</v-icon>
@@ -210,9 +211,9 @@
</v-col>
<!-- Right Column: Manual Input Card -->
<v-col cols="12" md="6" lg="5" class="manual-input-column">
<v-card class="manual-input-card" elevation="0">
<v-card-text class="pa-4">
<v-col cols="12" md="6" lg="4" class="manual-input-column d-flex">
<v-card class="manual-input-card" elevation="0" style="height: 100%; width: 100%;">
<v-card-text class="pa-4 d-flex flex-column" style="height: 100%;">
<!-- Status Header -->
<div class="status-header mb-3">
<div class="status-icon-wrapper">
@@ -395,74 +396,62 @@
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Enhanced Stats Sidebar - Right Side -->
<v-col cols="12" lg="auto" class="stats-sidebar-right" style="order: 3; flex-shrink: 0;">
<div class="sticky-wrapper">
<div class="stats-sidebar-enhanced">
<!-- Sidebar Header -->
<div class="sidebar-header">
<v-icon :color="primaryColor" size="14" class="mr-1">mdi-chart-box-outline</v-icon>
<span class="sidebar-title">Statistik</span>
<!-- Stats Footer (Horizontal) - Permanent Footer for All Screens -->
<v-row class="stats-footer-row" dense>
<v-col cols="12">
<div class="stats-footer-horizontal">
<div class="stat-item-footer-enhanced stat-item-footer-primary" :title="`Total Check-in Hari Ini: ${todayStats.total}`">
<div class="stat-icon-footer-enhanced">
<v-icon :color="primaryColor" size="18">mdi-calendar-today</v-icon>
</div>
<!-- Stats Items -->
<div class="stats-container">
<div class="stat-item-enhanced stat-item-primary" :title="`Total Check-in Hari Ini: ${todayStats.total}`">
<div class="stat-icon-enhanced">
<v-icon :color="primaryColor" size="18">mdi-calendar-today</v-icon>
</div>
<div class="stat-content-enhanced">
<div class="stat-value-enhanced">{{ todayStats.total }}</div>
<div class="stat-label-enhanced">Hari Ini</div>
</div>
</div>
<div class="stat-item-enhanced stat-item-success" :title="`Check-in Berhasil: ${todayStats.success}`">
<div class="stat-icon-enhanced">
<v-icon color="success" size="18">mdi-check-circle</v-icon>
</div>
<div class="stat-content-enhanced">
<div class="stat-value-enhanced">{{ todayStats.success }}</div>
<div class="stat-label-enhanced">Berhasil</div>
</div>
</div>
<div class="stat-item-enhanced stat-item-error" :title="`Check-in Gagal: ${todayStats.failed}`">
<div class="stat-icon-enhanced">
<v-icon color="error" size="18">mdi-close-circle</v-icon>
</div>
<div class="stat-content-enhanced">
<div class="stat-value-enhanced">{{ todayStats.failed }}</div>
<div class="stat-label-enhanced">Gagal</div>
</div>
</div>
<div class="stat-item-enhanced stat-item-info" :title="`Check-in via QR: ${todayStats.qr}`">
<div class="stat-icon-enhanced">
<v-icon color="info" size="18">mdi-qrcode-scan</v-icon>
</div>
<div class="stat-content-enhanced">
<div class="stat-value-enhanced">{{ todayStats.qr }}</div>
<div class="stat-label-enhanced">QR Scan</div>
</div>
</div>
<div class="stat-item-enhanced stat-item-warning" :title="`Check-in Manual: ${todayStats.manual}`">
<div class="stat-icon-enhanced">
<v-icon color="warning" size="18">mdi-keyboard</v-icon>
</div>
<div class="stat-content-enhanced">
<div class="stat-value-enhanced">{{ todayStats.manual }}</div>
<div class="stat-label-enhanced">Manual</div>
</div>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.total }}</div>
<div class="stat-label-footer-enhanced">Hari Ini</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-success" :title="`Check-in Berhasil: ${todayStats.success}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="success" size="18">mdi-check-circle</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.success }}</div>
<div class="stat-label-footer-enhanced">Berhasil</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-error" :title="`Check-in Gagal: ${todayStats.failed}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="error" size="18">mdi-close-circle</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.failed }}</div>
<div class="stat-label-footer-enhanced">Gagal</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-info" :title="`Check-in via QR: ${todayStats.qr}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="info" size="18">mdi-qrcode-scan</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.qr }}</div>
<div class="stat-label-footer-enhanced">QR Scan</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-warning" :title="`Check-in Manual: ${todayStats.manual}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="warning" size="18">mdi-keyboard</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.manual }}</div>
<div class="stat-label-footer-enhanced">Manual</div>
</div>
</div>
</div>
</v-col>
</v-row>
<!-- Generate QR Dialog -->
<v-dialog v-model="showGenerateQRDialog" max-width="600px" scrollable>
<v-card>
@@ -675,54 +664,7 @@
</v-card>
</v-dialog>
<!-- Stats Footer (Horizontal) - For Small Screens - Match Sidebar Design -->
<div class="stats-footer-horizontal mt-3">
<div class="stat-item-footer-enhanced stat-item-footer-primary" :title="`Total Check-in Hari Ini: ${todayStats.total}`">
<div class="stat-icon-footer-enhanced">
<v-icon :color="primaryColor" size="18">mdi-calendar-today</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.total }}</div>
<div class="stat-label-footer-enhanced">Hari Ini</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-success" :title="`Check-in Berhasil: ${todayStats.success}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="success" size="18">mdi-check-circle</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.success }}</div>
<div class="stat-label-footer-enhanced">Berhasil</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-error" :title="`Check-in Gagal: ${todayStats.failed}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="error" size="18">mdi-close-circle</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.failed }}</div>
<div class="stat-label-footer-enhanced">Gagal</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-info" :title="`Check-in via QR: ${todayStats.qr}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="info" size="18">mdi-qrcode-scan</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.qr }}</div>
<div class="stat-label-footer-enhanced">QR Scan</div>
</div>
</div>
<div class="stat-item-footer-enhanced stat-item-footer-warning" :title="`Check-in Manual: ${todayStats.manual}`">
<div class="stat-icon-footer-enhanced">
<v-icon color="warning" size="18">mdi-keyboard</v-icon>
</div>
<div class="stat-content-footer-enhanced">
<div class="stat-value-footer-enhanced">{{ todayStats.manual }}</div>
<div class="stat-label-footer-enhanced">Manual</div>
</div>
</div>
</div>
<!-- Enhanced Dialog with Blur -->
<v-dialog
@@ -3645,18 +3587,22 @@ onUnmounted(() => {
.checkin-page-container {
background: $neutral-100;
min-height: 100vh;
padding: 0 8px 16px 8px;
max-height: 100vh;
padding: 0 4px 0 4px;
width: 100%;
max-width: 100vw;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Header Full Width - No padding from edges */
.checkin-page-container :deep(.page-header) {
margin-left: -8px;
margin-right: -8px;
margin-left: -4px;
margin-right: -4px;
border-radius: 0 !important;
width: calc(100% + 16px);
width: calc(100% + 8px);
}
.checkin-page-container::before {
@@ -4098,16 +4044,19 @@ onUnmounted(() => {
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding: 20px 24px;
gap: 12px;
padding: 14px 16px;
background: linear-gradient(135deg, rgba($primary-50, 0.8) 0%, rgba($primary-100, 0.6) 100%);
border-radius: 16px;
border-radius: 12px;
border: 1px solid rgba($primary-600, 0.12);
box-shadow: 0 4px 12px rgba($primary-600, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.8);
text-align: left;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
min-height: 72px;
max-height: 72px;
flex-shrink: 0;
}
.status-header::before {
@@ -4127,9 +4076,13 @@ onUnmounted(() => {
}
.status-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
max-width: 40px;
max-height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
display: flex;
align-items: center;
@@ -4162,12 +4115,12 @@ onUnmounted(() => {
}
.status-title {
font-size: 16px;
font-weight: 700;
color: $neutral-900;
margin: 0 0 4px;
letter-spacing: -0.2px;
line-height: 1.3;
font-size: 15px !important;
font-weight: 700 !important;
color: $neutral-900 !important;
margin: 0 0 2px !important;
letter-spacing: -0.2px !important;
line-height: 1.3 !important;
}
.status-subtitle {
@@ -4183,8 +4136,8 @@ onUnmounted(() => {
display: flex;
justify-content: center;
align-items: center;
margin: 32px 0;
padding: 8px;
margin: 24px 0;
padding: 6px;
}
.qr-placeholder {
@@ -4432,9 +4385,9 @@ onUnmounted(() => {
font-weight: 600;
text-transform: none;
letter-spacing: 0.5px;
border-radius: 14px !important;
padding: 16px 32px !important;
min-height: 56px !important;
border-radius: 12px !important;
padding: 14px 28px !important;
min-height: 50px !important;
min-width: 44px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 6px 20px rgba($primary-600, 0.35), 0 2px 8px rgba($primary-600, 0.2) !important;
@@ -4486,9 +4439,9 @@ onUnmounted(() => {
font-weight: 600;
text-transform: none;
letter-spacing: 0.5px;
border-radius: 14px !important;
padding: 16px 32px !important;
min-height: 56px !important;
border-radius: 12px !important;
padding: 14px 28px !important;
min-height: 50px !important;
min-width: 44px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 6px 20px rgba($danger-500, 0.35), 0 2px 8px rgba($danger-500, 0.2) !important;
@@ -4828,10 +4781,11 @@ onUnmounted(() => {
/* Content Grid - Same pattern as AdminLoket.vue */
.content-grid {
margin: 0 20px;
}
.content-grid {
margin: 0 8px;
flex: 1;
min-height: 0;
overflow: visible;
align-items: stretch;
flex-wrap: nowrap !important;
}
@@ -4842,15 +4796,15 @@ onUnmounted(() => {
}
.content-grid > .v-col {
padding: 8px;
padding: 6px;
}
.content-grid > .v-col:first-child {
padding-left: 8px;
padding-left: 6px;
}
.content-grid > .v-col:last-child {
padding-right: 8px;
padding-right: 6px;
}
/* Ensure proper column order */
@@ -4882,8 +4836,10 @@ onUnmounted(() => {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba($primary-300, 0.2);
border-radius: 16px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
height: 100%;
}
@@ -4893,28 +4849,37 @@ onUnmounted(() => {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba($primary-300, 0.2);
border-radius: 16px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
min-height: 100%;
display: flex;
flex-direction: column;
height: 100%;
}
/* Removed main-content-col - now using content-grid pattern */
/* Column classes for equal height */
.sticky-column,
.manual-input-column {
display: flex;
flex-direction: column;
}
.sticky-wrapper {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
}
/* Hide right sidebar on all screens */
.stats-sidebar-right {
padding-left: 12px;
padding-right: 12px;
min-width: 160px;
max-width: 180px;
width: auto;
display: none;
align-items: flex-start;
justify-content: flex-start;
flex-shrink: 0 !important;
flex-grow: 0 !important;
position: relative;
height: auto;
display: none !important;
}
/* Content Grid */
.content-grid {
flex: 1;
min-height: 0;
overflow: visible;
order: 3 !important;
}
:deep(.stats-sidebar-right) {
@@ -4926,20 +4891,22 @@ onUnmounted(() => {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
align-items: flex-start;
justify-content: center;
align-items: flex-end;
justify-content: flex-end;
flex-shrink: 0 !important;
flex-grow: 0 !important;
width: auto !important;
max-width: 180px !important;
min-width: 160px !important;
max-width: 160px !important;
min-width: 140px !important;
z-index: 10;
margin-left: auto;
}
.stats-sidebar-enhanced {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
margin-left: auto;
}
}
@@ -4966,26 +4933,123 @@ onUnmounted(() => {
.sticky-wrapper {
top: 20px;
}
}
/* Compact Alert for Camera Warning */
.compact-alert {
padding: 4px 8px !important;
font-size: 10px !important;
min-height: 48px !important;
max-height: 48px !important;
border-radius: 8px !important;
.checkin-page-container {
padding-right: 0;
:deep(.v-alert__prepend) {
margin-right: 4px !important;
}
.checkin-page-container :deep(.page-header) {
margin-right: 0;
width: calc(100% + 8px);
:deep(.v-icon) {
font-size: 16px !important;
}
:deep(p) {
font-size: 9px !important;
line-height: 1.2 !important;
margin-bottom: 0 !important;
}
:deep(.font-weight-bold) {
font-size: 11px !important;
margin-bottom: 2px !important;
}
:deep(.text-body-2) {
font-size: 9px !important;
}
:deep(.v-alert__content) {
padding: 0 !important;
}
}
@media (max-width: 960px) {
.checkin-page-container {
padding: 0 12px 12px 12px;
padding: 0 8px 8px 8px;
max-height: none !important;
overflow-y: auto !important;
}
.checkin-page-container :deep(.page-header) {
margin-left: -12px;
margin-right: -12px;
width: calc(100% + 24px);
margin-left: -8px;
margin-right: -8px;
width: calc(100% + 16px);
}
.content-grid {
flex-direction: column !important;
margin: 0 4px !important;
}
.sticky-column,
.manual-input-column {
width: 100% !important;
max-width: 100% !important;
margin-bottom: 8px;
}
.qr-scan-card,
.manual-input-card {
max-height: 40vh !important;
overflow-y: auto !important;
padding: 12px;
}
/* Alert box mobile styles */
:deep(.v-alert) {
font-size: 12px !important;
padding: 10px 12px !important;
}
:deep(.v-alert .v-alert__prepend) {
margin-right: 8px !important;
}
:deep(.v-alert .v-icon) {
font-size: 20px !important;
}
:deep(.v-alert p) {
font-size: 11px !important;
line-height: 1.4 !important;
}
:deep(.v-alert .font-weight-bold) {
font-size: 13px !important;
}
/* QR Scanner mobile */
.qr-scanner-container {
margin: 12px 0;
}
.qr-placeholder {
max-width: 280px !important;
min-height: 180px !important;
}
/* Buttons mobile */
:deep(.v-btn) {
font-size: 13px !important;
min-height: 44px !important;
}
/* Input fields mobile */
:deep(.v-text-field) {
font-size: 14px !important;
}
:deep(.v-text-field .v-field__input) {
min-height: 44px !important;
padding: 8px 12px !important;
}
.sticky-wrapper {
@@ -4993,6 +5057,30 @@ onUnmounted(() => {
top: 0;
max-height: none;
}
.stats-footer-horizontal {
flex-wrap: wrap !important;
gap: 6px;
padding: 8px;
}
.stat-item-footer-enhanced {
min-width: calc(50% - 6px);
flex: 1 1 calc(50% - 6px);
padding: 8px 6px;
}
.stat-value-footer-enhanced {
font-size: 13px;
}
.stat-label-footer-enhanced {
font-size: 9px;
}
.stat-icon-footer-enhanced .v-icon {
font-size: 18px !important;
}
}
/* Enhanced Sidebar Design */
@@ -6121,13 +6209,20 @@ onUnmounted(() => {
}
}
/* Desktop - Large screens */
@media (min-width: 1264px) {
.content-grid {
align-items: flex-start;
justify-content: flex-start;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap !important;
}
.qr-scan-card,
.manual-input-card {
max-height: calc(100vh - 240px) !important;
overflow-y: auto;
}
.stats-sidebar-right {
margin-top: 0;
padding-top: 0;
@@ -6143,16 +6238,120 @@ onUnmounted(() => {
.sticky-column {
order: 1 !important;
flex-shrink: 0;
flex-basis: auto;
}
.content-grid > .v-col:nth-child(2) {
order: 2 !important;
flex-shrink: 0;
flex-basis: auto;
}
.stats-sidebar-enhanced,
.stats-sidebar-discord {
margin: 0 auto;
margin: 0;
}
}
/* Tablet - Medium screens (961px to 1263px) */
@media (min-width: 961px) and (max-width: 1263px) {
.qr-scan-card,
.manual-input-card {
max-height: calc(100vh - 220px) !important;
overflow-y: auto;
}
.content-grid {
align-items: stretch;
}
}
/* Small tablets and large phones (721px to 960px) */
@media (min-width: 721px) and (max-width: 960px) {
.qr-scan-card,
.manual-input-card {
max-height: calc(50vh - 100px) !important;
overflow-y: auto;
}
}
/* Very small screens (up to 720px) */
@media (max-width: 720px) {
.qr-scan-card,
.manual-input-card {
max-height: calc(45vh - 70px) !important;
overflow-y: auto;
}
.stats-footer-horizontal {
padding: 6px 8px;
gap: 4px;
}
}
/* Extra small screens (phones in portrait, up to 480px) */
@media (max-width: 480px) {
.checkin-page-container {
padding: 0 4px 4px 4px !important;
}
.content-grid {
margin: 0 2px !important;
}
.qr-scan-card,
.manual-input-card {
max-height: 38vh !important;
border-radius: 8px;
padding: 8px;
}
/* Alert box extra small */
:deep(.v-alert) {
font-size: 11px !important;
padding: 8px 10px !important;
border-radius: 8px !important;
}
:deep(.v-alert .v-alert__prepend) {
margin-right: 6px !important;
}
:deep(.v-alert .v-icon) {
font-size: 18px !important;
}
:deep(.v-alert p) {
font-size: 10px !important;
}
:deep(.v-alert .font-weight-bold) {
font-size: 12px !important;
}
.stats-footer-horizontal {
padding: 4px 6px;
gap: 3px;
border-radius: 8px;
max-width: calc(100vw - 12px) !important;
}
.stat-item-footer-enhanced {
min-width: calc(50% - 3px);
flex: 1 1 calc(50% - 3px);
padding: 6px 4px;
}
.stat-value-footer-enhanced {
font-size: 11px;
}
.stat-label-footer-enhanced {
font-size: 8px;
}
.stat-icon-footer-enhanced .v-icon {
font-size: 14px !important;
}
}
@@ -6174,18 +6373,32 @@ onUnmounted(() => {
}
}
/* Stats Footer Horizontal - For Small Screens - Match Sidebar Design */
.stats-footer-horizontal {
/* Stats Footer Horizontal - Permanent Footer for All Screens */
.stats-footer-row {
margin-top: 4px !important;
margin-bottom: 4px !important;
flex-shrink: 0;
display: flex;
justify-content: center;
}
.stats-footer-horizontal {
display: flex !important;
width: 100%;
max-width: calc(100vw - 32px);
gap: 5px;
padding: 8px;
padding: 6px 8px;
background: linear-gradient(180deg, rgba($primary-50, 0.98) 0%, rgba($primary-100, 0.92) 100%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid rgba($primary-300, 0.2);
justify-content: center;
align-items: center;
flex-wrap: nowrap;
overflow-x: auto;
margin: 0 auto;
}
.stat-item-footer-enhanced {
@@ -6197,11 +6410,11 @@ onUnmounted(() => {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
padding: 6px 4px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba($primary-300, 0.25);
overflow: hidden;
padding: 5px 4px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.6);
min-width: 65px;
gap: 2px;
}
.stat-item-footer-enhanced:hover {
@@ -6222,7 +6435,7 @@ onUnmounted(() => {
}
.stat-value-footer-enhanced {
font-size: 14px;
font-size: 13px;
font-weight: 700;
color: $primary-700;
line-height: 1.2;
@@ -6304,34 +6517,8 @@ onUnmounted(() => {
border-color: rgba($secondary-500, 0.5);
}
@media (max-width: 960px) {
.stats-footer-horizontal {
gap: 4px;
padding: 6px;
}
.stat-item-footer-enhanced {
padding: 5px 3px;
}
.stat-value-footer-enhanced {
font-size: 12px;
}
.stat-label-footer-enhanced {
font-size: 6px;
}
.stat-icon-footer-enhanced .v-icon {
font-size: 16px !important;
}
}
@media (min-width: 1264px) {
.stats-footer-horizontal {
display: none !important;
}
}
/* Mobile Landscape */
@media (max-width: 900px) and (orientation: landscape) {
+1 -1
View File
@@ -26,7 +26,7 @@ const defaultNavItems: NavItem[] = [
{ id: 4, name: "Admin Loket", icon: "mdi-account-supervisor-outline", path: "/AdminLoket" },
{ id: 6, name: "Admin Klinik Ruang", icon: "mdi-door-open", path: "/AdminKlinikRuang" },
// { id: 7, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
{ id: 8, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
// { id: 8, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
{ id: 9, name: "Monitoring Pasien", icon: "mdi-account-group-outline", path: "/MonitoringPasien/monitoringPasien" },
{
id: 10,