Files
web-antrean/pages/CheckInPasien/checkIn.vue
T

1068 lines
33 KiB
Vue

<template>
<v-app class="bg-light">
<v-main>
<v-container fluid class="fill-height pa-4">
<v-row align="center" justify="center">
<v-col cols="12" sm="10" md="8" lg="6">
<v-card class="elevation-20 rounded-xl overflow-hidden card-main" :style="cardStyle">
<!-- Header dengan gradient -->
<div class="header-gradient pa-6">
<div class="text-center">
<div class="hospital-icon-wrapper mb-3">
<v-icon size="56" color="white" class="pulse-animation">mdi-hospital-building</v-icon>
</div>
<h1 class="text-h4 font-weight-bold text-white mb-2">Check-in Pasien</h1>
<p class="text-subtitle-1 text-white opacity-90">Sistem Antrean Rumah Sakit</p>
</div>
<!-- Tabs dengan desain modern -->
<v-tabs
v-model="tab"
align-tabs="center"
class="mt-6 custom-tabs"
bg-color="transparent"
slider-color="white"
height="48"
>
<v-tab value="scan" class="text-white tab-item">
<v-icon start>mdi-qrcode-scan</v-icon>
Scan QR
</v-tab>
<v-tab value="manual" class="text-white tab-item">
<v-icon start>mdi-keyboard</v-icon>
Manual
</v-tab>
<v-tab value="generate" class="text-white tab-item">
<v-icon start>mdi-qrcode</v-icon>
Generate QR
</v-tab>
</v-tabs>
</div>
<v-card-text class="pa-8">
<v-window v-model="tab">
<!-- Tab Scan QR -->
<v-window-item value="scan">
<div class="scan-section">
<div class="text-center mb-6">
<v-icon :color="primaryColor" size="32" class="mb-2">mdi-information-outline</v-icon>
<p class="text-h6 font-weight-medium text-grey-darken-2">
Arahkan Kamera ke QR Code
</p>
<p class="text-body-2 text-grey">
Pastikan QR code terlihat jelas dan tidak terpotong
</p>
</div>
<!-- QR Scanner Area dengan animasi -->
<div class="qr-scanner-container mb-6">
<div class="qr-placeholder">
<div class="scanner-overlay">
<div class="corner corner-tl"></div>
<div class="corner corner-tr"></div>
<div class="corner corner-bl"></div>
<div class="corner corner-br"></div>
<div class="scan-line"></div>
</div>
<v-icon size="80" :color="primaryColor" class="qr-icon">mdi-qrcode-scan</v-icon>
</div>
</div>
<!-- Button dengan gradient -->
<v-btn
class="gradient-button"
block
size="x-large"
elevation="8"
@click="checkMockStatus"
>
<v-icon start>mdi-camera</v-icon>
Simulasi Scan QR
</v-btn>
<!-- Info tambahan -->
<div class="info-card mt-6">
<v-alert
type="info"
variant="tonal"
:color="primaryColor"
class="text-body-2"
>
<template v-slot:prepend>
<v-icon>mdi-lightbulb-outline</v-icon>
</template>
Tips: Pastikan pencahayaan cukup untuk hasil scan optimal
</v-alert>
</div>
</div>
</v-window-item>
<!-- Tab Manual -->
<v-window-item value="manual">
<div class="manual-section">
<div class="text-center mb-6">
<v-icon :color="secondaryColor" size="32" class="mb-2">mdi-form-textbox</v-icon>
<p class="text-h6 font-weight-medium text-grey-darken-2">
Input Manual
</p>
<p class="text-body-2 text-grey">
Masukkan nomor antrean atau ID pasien
</p>
</div>
<v-form @submit.prevent="checkInManual" ref="manualForm">
<v-text-field
v-model="manualInput"
label="Nomor Antrean / ID Pasien"
placeholder="Contoh: P12345"
prepend-inner-icon="mdi-account-card-details"
variant="outlined"
:color="primaryColor"
class="mb-2 custom-input"
required
:rules="[v => !!v || 'Field ini wajib diisi']"
density="comfortable"
>
<template v-slot:append-inner>
<v-icon :color="manualInput ? 'success' : 'grey'">
{{ manualInput ? 'mdi-check-circle' : 'mdi-circle-outline' }}
</v-icon>
</template>
</v-text-field>
<v-btn
:color="secondaryColor"
class="text-white font-weight-bold text-none gradient-button-secondary"
block
size="x-large"
type="submit"
elevation="8"
>
<v-icon start>mdi-login</v-icon>
Check-in Sekarang
</v-btn>
</v-form>
<!-- Quick Access Buttons -->
<div class="quick-actions mt-6">
<p class="text-caption text-grey text-center mb-3">Akses Cepat</p>
<v-row dense>
<v-col cols="6">
<v-btn
variant="outlined"
:color="primaryColor"
block
size="small"
class="text-none"
>
<v-icon start size="18">mdi-history</v-icon>
Riwayat
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
variant="outlined"
:color="primaryColor"
block
size="small"
class="text-none"
>
<v-icon start size="18">mdi-help-circle</v-icon>
Bantuan
</v-btn>
</v-col>
</v-row>
</div>
</div>
</v-window-item>
<!-- Tab Generate QR -->
<v-window-item value="generate">
<div class="generate-section">
<div class="text-center mb-6">
<v-icon :color="primaryColor" size="32" class="mb-2">mdi-qrcode</v-icon>
<p class="text-h6 font-weight-medium text-grey-darken-2">
Generate QR Code untuk Testing
</p>
<p class="text-body-2 text-grey">
Buat QR code yang bisa Anda scan di tab "Scan QR"
</p>
</div>
<!-- Form Generate -->
<v-form @submit.prevent="generateQRCode">
<v-text-field
v-model="generatePatientId"
label="ID Pasien"
placeholder="Contoh: P12345"
prepend-inner-icon="mdi-identifier"
variant="outlined"
:color="primaryColor"
class="mb-4 custom-input"
density="comfortable"
></v-text-field>
<v-select
v-model="generateStatus"
label="Status Check-in"
:items="statusOptions"
prepend-inner-icon="mdi-shield-check"
variant="outlined"
:color="primaryColor"
class="mb-4 custom-input"
density="comfortable"
></v-select>
<v-btn
:color="primaryColor"
class="gradient-button"
block
size="x-large"
type="submit"
elevation="8"
>
<v-icon start>mdi-qrcode-plus</v-icon>
Generate QR Code
</v-btn>
</v-form>
<!-- QR Code Display -->
<div v-if="generatedQRData" class="qr-display mt-6">
<v-card variant="outlined" class="pa-4">
<div class="text-center">
<p class="text-subtitle-2 text-grey mb-3">QR Code Anda:</p>
<div id="qrcode" class="qr-code-container mb-4"></div>
<v-chip :color="generateStatus === 'ALLOWED' ? 'success' : 'warning'" class="mb-3">
<v-icon start>{{ generateStatus === 'ALLOWED' ? 'mdi-check' : 'mdi-clock-alert' }}</v-icon>
{{ generateStatus === 'ALLOWED' ? 'Diizinkan Check-in' : 'Belum Diizinkan' }}
</v-chip>
<p class="text-body-2 text-grey mb-4">
Data: {{ generatedQRData }}
</p>
<!-- Action Buttons -->
<v-row dense>
<v-col cols="6">
<v-btn
variant="outlined"
:color="primaryColor"
block
size="small"
@click="downloadQR"
class="text-none"
>
<v-icon start size="18">mdi-download</v-icon>
Download
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
variant="outlined"
:color="primaryColor"
block
size="small"
@click="shareQR"
class="text-none"
>
<v-icon start size="18">mdi-share-variant</v-icon>
Share
</v-btn>
</v-col>
</v-row>
</div>
</v-card>
<!-- Instructions -->
<v-alert
type="success"
variant="tonal"
class="mt-4 text-body-2"
>
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
<strong>Cara menggunakan:</strong>
<ol class="ml-4 mt-2">
<li>Klik "Download" atau "Share" untuk menyimpan/mengirim QR ke HP Anda</li>
<li>Buka QR code di HP Anda</li>
<li>Pindah ke tab "Scan QR" dan scan QR code tersebut</li>
</ol>
</v-alert>
</div>
</div>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<!-- Stats Footer -->
<div class="stats-footer mt-4">
<v-row>
<v-col cols="4">
<div class="stat-card">
<v-icon :color="primaryColor" size="24">mdi-clock-outline</v-icon>
<div class="text-caption text-grey mt-1">Hari Ini</div>
<div class="text-h6 font-weight-bold">24</div>
</div>
</v-col>
<v-col cols="4">
<div class="stat-card">
<v-icon color="success" size="24">mdi-check-circle</v-icon>
<div class="text-caption text-grey mt-1">Selesai</div>
<div class="text-h6 font-weight-bold">18</div>
</div>
</v-col>
<v-col cols="4">
<div class="stat-card">
<v-icon :color="secondaryColor" size="24">mdi-account-group</v-icon>
<div class="text-caption text-grey mt-1">Menunggu</div>
<div class="text-h6 font-weight-bold">6</div>
</div>
</v-col>
</v-row>
</div>
</v-col>
</v-row>
</v-container>
</v-main>
<!-- Enhanced Dialog with Blur -->
<v-dialog
v-model="infoDialog"
max-width="500"
persistent
transition="dialog-transition"
scrim="rgba(0, 0, 0, 0.5)"
class="blur-dialog"
>
<v-card class="rounded-xl dialog-card" elevation="24">
<div class="dialog-header text-center pa-8" :class="infoAction === 'checkin' ? 'success-header' : 'warning-header'">
<div class="decorative-circles">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
<div class="icon-wrapper mb-4">
<div class="icon-bg" :class="infoAction === 'checkin' ? 'icon-bg-success' : 'icon-bg-warning'">
<v-icon size="64" color="white" class="dialog-icon">
{{ infoAction === 'checkin' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
</v-icon>
</div>
</div>
<h2 class="text-h4 font-weight-bold text-white mb-2">
{{ infoAction === 'checkin' ? 'Siap Check-in!' : 'Belum Diizinkan' }}
</h2>
<div class="status-badge mt-3">
<v-chip
:color="infoAction === 'checkin' ? 'white' : 'white'"
:text-color="infoAction === 'checkin' ? 'success' : 'orange'"
size="small"
class="font-weight-bold"
>
<v-icon start size="16">
{{ infoAction === 'checkin' ? 'mdi-check' : 'mdi-clock-alert' }}
</v-icon>
{{ infoAction === 'checkin' ? 'Diizinkan' : 'Menunggu' }}
</v-chip>
</div>
</div>
<v-card-text class="pa-8">
<div class="message-container">
<div class="message-icon mb-4">
<v-icon :color="infoAction === 'checkin' ? 'success' : 'orange'" size="32">
mdi-information-outline
</v-icon>
</div>
<p class="text-h6 font-weight-bold mb-3 text-center">{{ infoMessage }}</p>
<v-divider class="my-4"></v-divider>
<div class="instruction-box pa-4 rounded-lg" :class="infoAction === 'checkin' ? 'instruction-success' : 'instruction-warning'">
<div class="d-flex align-start">
<v-icon :color="infoAction === 'checkin' ? 'success' : 'orange'" class="mr-3 mt-1">
{{ infoAction === 'checkin' ? 'mdi-hand-pointing-down' : 'mdi-timer-sand' }}
</v-icon>
<div>
<p class="text-body-1 font-weight-medium mb-1">
{{ infoAction === 'checkin' ? 'Langkah Selanjutnya:' : 'Informasi:' }}
</p>
<p class="text-body-2 text-grey-darken-1">
{{ infoAction === 'checkin' ? 'Klik tombol Check-in di bawah untuk melanjutkan proses registrasi pasien' : 'Mohon menunggu hingga antrean Anda dipanggil oleh petugas' }}
</p>
</div>
</div>
</div>
<!-- Patient Info Card -->
<v-card variant="outlined" class="mt-4 patient-info-card" color="grey-lighten-4">
<v-card-text class="py-3">
<div class="d-flex justify-space-between align-center">
<div class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-account-circle</v-icon>
<div>
<p class="text-caption text-grey mb-0">ID Pasien</p>
<p class="text-body-1 font-weight-bold mb-0">{{ scannedData?.split('|')[0] || 'N/A' }}</p>
</div>
</div>
<div class="text-right">
<p class="text-caption text-grey mb-0">Waktu Scan</p>
<p class="text-body-2 font-weight-medium mb-0">{{ new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }) }}</p>
</div>
</div>
</v-card-text>
</v-card>
</div>
</v-card-text>
<v-card-actions class="pa-6 pt-0">
<v-row dense>
<v-col v-if="infoAction === 'kembali'" cols="12">
<v-btn
color="grey-darken-1"
class="text-white font-weight-bold text-none dialog-button"
size="x-large"
block
variant="flat"
@click="handleInfoAction"
elevation="0"
prepend-icon="mdi-arrow-left"
>
Kembali
</v-btn>
</v-col>
<template v-else>
<v-col cols="6">
<v-btn
color="grey-lighten-1"
class="text-none dialog-button"
size="x-large"
block
variant="outlined"
@click="infoDialog = false"
prepend-icon="mdi-close"
>
Batal
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
color="success"
class="text-white font-weight-bold text-none dialog-button"
size="x-large"
block
variant="flat"
@click="handleInfoAction"
elevation="4"
prepend-icon="mdi-login"
>
Check-in
</v-btn>
</v-col>
</template>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Enhanced Snackbar -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="bottom right"
rounded="pill"
elevation="24"
class="custom-snackbar"
>
<div class="d-flex align-center">
<v-avatar :color="snackbar.color" size="32" class="mr-3">
<v-icon size="20" color="white">{{ snackbar.icon }}</v-icon>
</v-avatar>
<div>
<div class="font-weight-bold">{{ snackbar.title }}</div>
<div class="text-body-2">{{ snackbar.message }}</div>
</div>
</div>
</v-snackbar>
</v-app>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
definePageMeta({
middleware:['auth']
})
// TypeScript declaration for QRCode
declare global {
interface Window {
QRCode: any;
}
}
// --- DESAIN & TEMA ---
const primaryColor = ref('#1565C0');
const secondaryColor = ref('#FB8C00');
const cardStyle = computed(() => ({
background: 'white',
}));
// --- LOGIKA ---
const tab = ref('scan');
const infoDialog = ref(false);
const infoMessage = ref('');
const infoAction = ref<'checkin' | 'kembali'>('kembali');
const scannedData = ref<string | null>(null);
const manualInput = ref('');
const manualForm = ref(null);
// Generate QR variables
const generatePatientId = ref('P12345');
const generateStatus = ref('ALLOWED');
const generatedQRData = ref('');
const statusOptions = [
{ title: 'Diizinkan Check-in', value: 'ALLOWED' },
{ title: 'Belum Diizinkan', value: 'NOT_ALLOWED' }
];
const snackbar = ref({
show: false,
title: '',
message: '',
color: '',
icon: '',
timeout: 4000,
});
const checkMockStatus = () => {
const isCurrentlyAllowed = infoAction.value === 'checkin';
const mockData = isCurrentlyAllowed ? 'P67890|NOT_ALLOWED' : 'P12345|ALLOWED';
scannedData.value = mockData;
onDetect(mockData);
};
const onDetect = (decodedText: string) => {
const [patientId, status] = decodedText.split('|');
if (status === 'ALLOWED') {
infoMessage.value = `Antrean Pasien ${patientId} sudah diperbolehkan check-in`;
infoAction.value = 'checkin';
infoDialog.value = true;
} else if (status === 'NOT_ALLOWED') {
infoMessage.value = `Antrean Pasien ${patientId} belum diperbolehkan check-in`;
infoAction.value = 'kembali';
infoDialog.value = true;
} else {
showSnackbar('Gagal', 'QR Code tidak valid.', 'error', 'mdi-close-circle');
}
};
const handleInfoAction = async () => {
infoDialog.value = false;
if (infoAction.value === 'checkin') {
const checkinSuccess = await performCheckIn(scannedData.value!);
if (checkinSuccess) {
showSnackbar('Berhasil!', 'Check-in antrean berhasil dilakukan.', 'success', 'mdi-check-circle');
} else {
showSnackbar('Gagal!', 'Check-in antrean gagal dilakukan. Silakan coba lagi!', 'error', 'mdi-close-circle');
}
}
};
const performCheckIn = async (data: string): Promise<boolean> => {
await new Promise(resolve => setTimeout(resolve, 1000));
return Math.random() < 0.8;
};
const showSnackbar = (title: string, message: string, color: string, icon: string) => {
snackbar.value.title = title;
snackbar.value.message = message;
snackbar.value.color = color;
snackbar.value.icon = icon;
snackbar.value.show = true;
};
const checkInManual = () => {
if (manualForm.value) {
showSnackbar('Info', 'Check-in Manual sedang diproses.', 'info', 'mdi-information');
}
};
// Generate QR Code function
const generateQRCode = async () => {
if (!generatePatientId.value) {
showSnackbar('Error', 'Mohon isi ID Pasien', 'error', 'mdi-alert');
return;
}
generatedQRData.value = `${generatePatientId.value}|${generateStatus.value}`;
await nextTick();
// Clear previous QR code
const qrContainer = document.getElementById('qrcode');
if (qrContainer) {
qrContainer.innerHTML = '';
// Load QRCode.js from CDN if not already loaded
if (typeof window.QRCode === 'undefined') {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js';
script.onload = () => createQR();
document.head.appendChild(script);
} else {
createQR();
}
}
showSnackbar('Berhasil!', 'QR Code berhasil di-generate', 'success', 'mdi-check-circle');
};
const createQR = () => {
const qrContainer = document.getElementById('qrcode');
if (qrContainer && window.QRCode) {
new window.QRCode(qrContainer, {
text: generatedQRData.value,
width: 256,
height: 256,
colorDark: '#1565C0',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
};
const downloadQR = () => {
const canvas = document.querySelector('#qrcode canvas') as HTMLCanvasElement;
if (canvas) {
const url = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = `QR-${generatePatientId.value}.png`;
link.href = url;
link.click();
showSnackbar('Berhasil!', 'QR Code berhasil didownload', 'success', 'mdi-download');
}
};
const shareQR = async () => {
const canvas = document.querySelector('#qrcode canvas') as HTMLCanvasElement;
if (canvas) {
canvas.toBlob(async (blob) => {
if (blob) {
const file = new File([blob], `QR-${generatePatientId.value}.png`, { type: 'image/png' });
if (navigator.share && navigator.canShare({ files: [file] })) {
try {
await navigator.share({
files: [file],
title: 'QR Code Check-in',
text: `QR Code untuk pasien ${generatePatientId.value}`
});
showSnackbar('Berhasil!', 'QR Code berhasil dibagikan', 'success', 'mdi-share');
} catch (err: any) {
if (err.name !== 'AbortError') {
showSnackbar('Info', 'Gagal membagikan. Silakan download dan kirim manual.', 'info', 'mdi-information');
}
}
} else {
// Fallback: copy to clipboard atau download
showSnackbar('Info', 'Browser tidak support share. Silakan gunakan Download.', 'info', 'mdi-information');
}
}
});
}
};
</script>
<style scoped>
.bg-light {
background: #f5f7fa;
min-height: 100vh;
}
.card-main {
transition: all 0.3s ease;
}
.card-main:hover {
transform: translateY(-4px);
}
.header-gradient {
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%);
position: relative;
overflow: hidden;
}
.header-gradient::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.hospital-icon-wrapper {
display: inline-block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.pulse-animation {
animation: pulse-icon 2s ease-in-out infinite;
}
@keyframes pulse-icon {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.custom-tabs :deep(.v-tab) {
text-transform: none;
font-weight: 600;
letter-spacing: 0.5px;
font-size: 0.9rem;
}
.custom-tabs :deep(.v-tab--selected) {
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
.scan-section, .manual-section, .generate-section {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.qr-scanner-container {
display: flex;
justify-content: center;
align-items: center;
}
.qr-placeholder {
width: 100%;
max-width: 320px;
height: 320px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 16px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: inset 0 0 20px rgba(0,0,0,0.1);
}
.scanner-overlay {
position: absolute;
width: 80%;
height: 80%;
}
.corner {
position: absolute;
width: 40px;
height: 40px;
border: 3px solid #1565C0;
}
.corner-tl {
top: 0;
left: 0;
border-right: none;
border-bottom: none;
border-radius: 8px 0 0 0;
}
.corner-tr {
top: 0;
right: 0;
border-left: none;
border-bottom: none;
border-radius: 0 8px 0 0;
}
.corner-bl {
bottom: 0;
left: 0;
border-right: none;
border-top: none;
border-radius: 0 0 0 8px;
}
.corner-br {
bottom: 0;
right: 0;
border-left: none;
border-top: none;
border-radius: 0 0 8px 0;
}
.scan-line {
position: absolute;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #1565C0, transparent);
top: 0;
animation: scan 2s linear infinite;
box-shadow: 0 0 10px #1565C0;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.qr-icon {
position: relative;
z-index: 1;
animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.gradient-button {
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%) !important;
color: white !important;
font-weight: 700;
text-transform: none;
letter-spacing: 0.5px;
transition: all 0.3s ease;
}
.gradient-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 101, 192, 0.4) !important;
}
.gradient-button-secondary {
background: linear-gradient(135deg, #FB8C00 0%, #F57C00 100%) !important;
color: white !important;
font-weight: 700;
text-transform: none;
letter-spacing: 0.5px;
transition: all 0.3s ease;
}
.gradient-button-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(251, 140, 0, 0.4) !important;
}
.custom-input :deep(.v-field) {
border-radius: 12px;
font-size: 16px;
}
.custom-input :deep(.v-field--focused) {
box-shadow: 0 0 0 3px rgba(21, 101, 192, 0.1);
}
.quick-actions .v-btn {
transition: all 0.3s ease;
}
.quick-actions .v-btn:hover {
transform: translateY(-2px);
}
.qr-code-container {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.qr-code-container :deep(canvas) {
border-radius: 8px;
}
.qr-display {
animation: slideUp 0.5s ease-out;
}
.blur-dialog :deep(.v-overlay__scrim) {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.dialog-card {
overflow: hidden;
animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
0% {
opacity: 0;
transform: scale(0.8) translateY(20px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.dialog-header {
position: relative;
overflow: hidden;
}
.dialog-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%);
animation: rotate 10s linear infinite;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.icon-wrapper {
display: inline-block;
animation: bounceIn 0.6s ease-out 0.2s both;
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.dialog-icon {
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.3));
}
.success-header {
background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%);
}
.warning-header {
background: linear-gradient(135deg, #FFA726 0%, #FB8C00 100%);
}
.dialog-button {
text-transform: none;
letter-spacing: 0.5px;
font-weight: 600;
transition: all 0.3s ease;
}
.dialog-button:hover {
transform: translateY(-2px);
}
.stats-footer {
animation: slideUp 0.5s ease-out 0.3s both;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.stat-card {
background: white;
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.custom-snackbar {
margin-bottom: 16px;
margin-right: 16px;
}
.custom-snackbar :deep(.v-snackbar__wrapper) {
min-width: 300px;
}
/* Mobile Optimization */
@media (max-width: 600px) {
.v-container.fill-height {
padding: 8px !important;
}
.card-main {
margin: 0;
}
.header-gradient {
padding: 24px 16px !important;
}
.qr-placeholder {
max-width: 280px;
height: 280px;
}
.stats-footer {
display: none;
}
.custom-tabs :deep(.v-tab) {
font-size: 0.75rem;
padding: 0 8px;
}
}
</style>