717 lines
25 KiB
Plaintext
717 lines
25 KiB
Plaintext
<template>
|
|
<v-app class="bg-modern">
|
|
<v-main class="no-overflow">
|
|
<v-container fluid class="no-scroll-container pa-2 pa-md-4">
|
|
<v-row align="start" justify="center" class="fill-height">
|
|
<v-col cols="12" sm="11" md="10" lg="8" xl="7" class="d-flex flex-column">
|
|
<!-- Main Card dengan Glassmorphism -->
|
|
<v-card class="main-card" elevation="0">
|
|
|
|
<!-- Header Minimalis -->
|
|
<div class="header-modern">
|
|
<div class="header-content">
|
|
<div class="icon-circle">
|
|
<v-icon size="36" color="white">mdi-hospital-building</v-icon>
|
|
</div>
|
|
<div class="header-text">
|
|
<h1 class="title-modern">Check-in Pasien</h1>
|
|
<p class="subtitle-modern">Sistem Antrean Rumah Sakit</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Minimalis -->
|
|
<v-tabs
|
|
v-model="tab"
|
|
align-tabs="center"
|
|
class="tabs-modern mt-3"
|
|
bg-color="transparent"
|
|
slider-color="#FB8C00"
|
|
height="40"
|
|
>
|
|
<v-tab value="scan" class="tab-modern">
|
|
<v-icon size="20" class="mr-2">mdi-qrcode-scan</v-icon>
|
|
<span>Scan QR</span>
|
|
</v-tab>
|
|
<v-tab value="manual" class="tab-modern">
|
|
<v-icon size="20" class="mr-2">mdi-keyboard</v-icon>
|
|
<span>Manual</span>
|
|
</v-tab>
|
|
<v-tab value="generate" class="tab-modern">
|
|
<v-icon size="20" class="mr-2">mdi-qrcode</v-icon>
|
|
<span>Generate QR</span>
|
|
</v-tab>
|
|
</v-tabs>
|
|
</div>
|
|
|
|
<v-card-text class="content-modern pa-4 pa-md-6">
|
|
<v-window v-model="tab">
|
|
<!-- Tab Scan QR -->
|
|
<v-window-item value="scan">
|
|
<QRScanTab
|
|
:is-scanning="isScanning"
|
|
:has-camera="hasCamera"
|
|
:camera-checking="cameraChecking"
|
|
:camera-ready="cameraReady"
|
|
:primary-color="primaryColor"
|
|
@start-scanning="startScanning"
|
|
@stop-scanning="stopScanning"
|
|
@test-camera="testCamera"
|
|
@open-history="openHistoryDialog"
|
|
@open-qr-history="openQRHistoryDialog"
|
|
/>
|
|
</v-window-item>
|
|
|
|
<!-- Tab Manual -->
|
|
<v-window-item value="manual">
|
|
<ManualInputTab
|
|
v-model="manualInput"
|
|
:primary-color="primaryColor"
|
|
:secondary-color="secondaryColor"
|
|
@submit="checkInManual"
|
|
@open-history="openHistoryDialog"
|
|
ref="manualInputTabRef"
|
|
/>
|
|
</v-window-item>
|
|
|
|
<!-- Tab Generate QR -->
|
|
<v-window-item value="generate">
|
|
<div class="tab-content">
|
|
<!-- Status Header -->
|
|
<div class="status-header mb-3">
|
|
<div class="status-icon-wrapper">
|
|
<v-icon :color="primaryColor" size="24">mdi-qrcode</v-icon>
|
|
</div>
|
|
<div class="status-text">
|
|
<h3 class="status-title">Generate QR Code untuk Testing</h3>
|
|
<p class="status-subtitle">Buat QR code yang bisa Anda scan di tab "Scan QR"</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Preset Buttons -->
|
|
<div class="mb-3">
|
|
<p class="text-caption text-grey text-center mb-2">Quick Test QR Codes:</p>
|
|
<v-row dense>
|
|
<v-col cols="6">
|
|
<v-btn
|
|
variant="outlined"
|
|
color="success"
|
|
size="small"
|
|
@click="generateQuickQR(generateRandomPatientId(), 'ALLOWED')"
|
|
class="text-none"
|
|
block
|
|
>
|
|
<v-icon start size="16">mdi-check-circle</v-icon>
|
|
Test ALLOWED
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<v-btn
|
|
variant="outlined"
|
|
color="warning"
|
|
size="small"
|
|
@click="generateQuickQR(generateRandomPatientId(), 'NOT_ALLOWED')"
|
|
class="text-none"
|
|
block
|
|
>
|
|
<v-icon start size="16">mdi-clock-alert</v-icon>
|
|
Test NOT_ALLOWED
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Form Generate -->
|
|
<v-form @submit.prevent="generateQRCode">
|
|
<v-text-field
|
|
v-model="generatePatientId"
|
|
label="ID Pasien"
|
|
placeholder="Contoh: P-123456"
|
|
prepend-inner-icon="mdi-identifier"
|
|
variant="outlined"
|
|
:color="primaryColor"
|
|
class="input-modern mb-3"
|
|
density="comfortable"
|
|
clearable
|
|
hide-details="auto"
|
|
>
|
|
<template v-slot:append-inner>
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
@click="generateRandomId()"
|
|
title="Generate Random ID"
|
|
>
|
|
<v-icon size="20">mdi-refresh</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</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="input-modern mb-4"
|
|
density="comfortable"
|
|
hide-details="auto"
|
|
></v-select>
|
|
|
|
<div class="d-flex justify-center">
|
|
<v-btn
|
|
class="btn-primary-modern btn-centered"
|
|
size="large"
|
|
type="submit"
|
|
elevation="0"
|
|
:disabled="!generatePatientId"
|
|
>
|
|
<v-icon start size="20">mdi-qrcode-plus</v-icon>
|
|
Generate QR Code
|
|
</v-btn>
|
|
</div>
|
|
</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="4">
|
|
<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="4">
|
|
<v-btn
|
|
variant="outlined"
|
|
:color="primaryColor"
|
|
block
|
|
size="small"
|
|
@click="copyQRToClipboard"
|
|
class="text-none"
|
|
>
|
|
<v-icon start size="18">mdi-content-copy</v-icon>
|
|
Copy
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="4">
|
|
<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 untuk Testing:</strong>
|
|
<ol class="ml-4 mt-2">
|
|
<li>Gunakan tombol <strong>"Test ALLOWED"</strong> atau <strong>"Test NOT_ALLOWED"</strong> untuk generate QR cepat, atau isi form manual</li>
|
|
<li>Klik <strong>"Download"</strong> untuk menyimpan QR code ke komputer</li>
|
|
<li>Buka file QR code yang didownload (bisa di HP atau layar lain)</li>
|
|
<li>Pindah ke tab <strong>"Scan QR"</strong> dan scan QR code tersebut</li>
|
|
<li>Atau gunakan <strong>"Copy"</strong> untuk menyalin QR ke clipboard dan paste di aplikasi lain</li>
|
|
</ol>
|
|
</v-alert>
|
|
</div>
|
|
</div>
|
|
</v-window-item>
|
|
</v-window>
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<!-- Stats Footer Minimalis -->
|
|
<div class="stats-footer-modern mt-3">
|
|
<v-row dense>
|
|
<v-col cols="4">
|
|
<div class="stat-card-modern">
|
|
<div class="stat-icon-modern">
|
|
<v-icon :color="primaryColor" size="20">mdi-clock-outline</v-icon>
|
|
</div>
|
|
<div class="stat-value">{{ todayCount }}</div>
|
|
<div class="stat-label">Hari Ini</div>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="4">
|
|
<div class="stat-card-modern">
|
|
<div class="stat-icon-modern">
|
|
<v-icon color="success" size="20">mdi-check-circle</v-icon>
|
|
</div>
|
|
<div class="stat-value">{{ completedCount }}</div>
|
|
<div class="stat-label">Selesai</div>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="4">
|
|
<div class="stat-card-modern">
|
|
<div class="stat-icon-modern">
|
|
<v-icon :color="secondaryColor" size="20">mdi-account-group</v-icon>
|
|
</div>
|
|
<div class="stat-value">{{ pendingCount }}</div>
|
|
<div class="stat-label">Menunggu</div>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</v-main>
|
|
|
|
<!-- Check-in Result Dialog -->
|
|
<CheckInDialog
|
|
v-model="infoDialog"
|
|
:last-check-in-result="lastCheckInResult"
|
|
:info-message="infoMessage"
|
|
:info-action="infoAction"
|
|
:scanned-data="scannedData"
|
|
@close="handleInfoAction"
|
|
/>
|
|
|
|
<!-- Enhanced Snackbar -->
|
|
<v-snackbar
|
|
v-model="snackbarShow"
|
|
: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>
|
|
|
|
<!-- History Dialog -->
|
|
<HistoryDialog
|
|
v-model="historyDialog"
|
|
:search="historySearch"
|
|
:date-filter="historyDateFilter"
|
|
:status-filter="historyStatusFilter"
|
|
:filtered-history="filteredHistory"
|
|
:history="checkInHistory"
|
|
:status-options="historyStatusOptions"
|
|
:primary-color="primaryColor"
|
|
:get-status-color="getStatusColor"
|
|
:get-status-icon="getStatusIcon"
|
|
:get-status-text="getStatusText"
|
|
:get-status-class="getStatusClass"
|
|
:format-date-time="formatDateTime"
|
|
:format-date="formatDate"
|
|
@update:search="historySearch = $event"
|
|
@update:date-filter="historyDateFilter = $event"
|
|
@update:status-filter="historyStatusFilter = $event"
|
|
@delete-item="handleDeleteHistoryItem"
|
|
@clear="handleClearHistory"
|
|
/>
|
|
|
|
<!-- QR History Dialog -->
|
|
<QRHistoryDialog
|
|
v-model="qrHistoryDialog"
|
|
:search="historySearch"
|
|
:date-filter="historyDateFilter"
|
|
:filtered-history="filteredQRHistory"
|
|
:history="scannedQRHistory"
|
|
:primary-color="primaryColor"
|
|
@update:search="historySearch = $event"
|
|
@update:date-filter="historyDateFilter = $event"
|
|
@use-data="useQRData"
|
|
@delete-item="handleDeleteQRHistoryItem"
|
|
@clear="handleClearQRHistory"
|
|
/>
|
|
|
|
</v-app>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
import type { TabValue, InfoAction, CheckInHistoryItem } from '~/types/checkin';
|
|
import { PRIMARY_COLOR, SECONDARY_COLOR, QR_STATUS_OPTIONS } from '~/constants/checkin';
|
|
import { useSnackbar } from '~/composables/useSnackbar';
|
|
import { useCheckInHistory } from '~/composables/useCheckInHistory';
|
|
import { useQRScanner } from '~/composables/useQRScanner';
|
|
import { useCheckIn } from '~/composables/useCheckIn';
|
|
import { useQRGenerator } from '~/composables/useQRGenerator';
|
|
import CheckInDialog from '~/components/checkin/CheckInDialog.vue';
|
|
import HistoryDialog from '~/components/checkin/HistoryDialog.vue';
|
|
import QRHistoryDialog from '~/components/checkin/QRHistoryDialog.vue';
|
|
import ManualInputTab from '~/components/checkin/ManualInputTab.vue';
|
|
import QRScanTab from '~/components/checkin/QRScanTab.vue';
|
|
|
|
definePageMeta({
|
|
middleware:['auth'],
|
|
layout: false,
|
|
})
|
|
|
|
// TypeScript declaration for QRCode
|
|
declare global {
|
|
interface Window {
|
|
QRCode: typeof import('qrcode').default;
|
|
}
|
|
}
|
|
|
|
// --- DESAIN & TEMA ---
|
|
const primaryColor = ref(PRIMARY_COLOR);
|
|
const secondaryColor = ref(SECONDARY_COLOR);
|
|
|
|
// --- LOGIKA ---
|
|
const tab = ref<TabValue>('scan');
|
|
const infoDialog = ref(false);
|
|
const infoMessage = ref('');
|
|
const infoAction = ref<InfoAction>('kembali');
|
|
const scannedData = ref<string | null>(null);
|
|
const manualInput = ref('');
|
|
const manualInputTabRef = ref<InstanceType<typeof ManualInputTab> | null>(null);
|
|
// lastCheckInResult sudah dipindahkan ke useCheckIn composable
|
|
|
|
// Timer untuk auto-close dialog
|
|
let dialogAutoCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
// Fungsi untuk auto-close dialog setelah 15 detik
|
|
const autoCloseDialog = () => {
|
|
// Clear timer sebelumnya jika ada
|
|
if (dialogAutoCloseTimer) {
|
|
clearTimeout(dialogAutoCloseTimer);
|
|
}
|
|
|
|
// Set timer untuk menutup dialog setelah 15 detik
|
|
dialogAutoCloseTimer = setTimeout(() => {
|
|
if (infoDialog.value) {
|
|
infoDialog.value = false;
|
|
dialogAutoCloseTimer = null;
|
|
}
|
|
}, 15000); // 15 detik
|
|
};
|
|
|
|
// Snackbar composable (harus didefinisikan sebelum useQRScanner)
|
|
const { snackbar, snackbarShow, showSnackbar } = useSnackbar();
|
|
|
|
// History composable
|
|
const {
|
|
checkInHistory,
|
|
scannedQRHistory,
|
|
historySearch,
|
|
historyDateFilter,
|
|
historyStatusFilter,
|
|
filteredHistory,
|
|
filteredQRHistory,
|
|
loadHistory,
|
|
saveToHistory,
|
|
deleteHistoryItem,
|
|
clearHistory,
|
|
loadScannedQRHistory,
|
|
saveScannedQRData,
|
|
clearQRHistory,
|
|
deleteQRHistoryItem,
|
|
getStatusColor,
|
|
getStatusIcon,
|
|
getStatusText,
|
|
getStatusClass,
|
|
formatDateTime,
|
|
formatDate,
|
|
historyStatusOptions,
|
|
} = useCheckInHistory();
|
|
|
|
// Computed untuk stats footer
|
|
const todayCount = computed(() => {
|
|
const today = new Date().toDateString()
|
|
return checkInHistory.value.filter((item: CheckInHistoryItem) => {
|
|
const itemDate = new Date(item.checkInDate).toDateString()
|
|
return itemDate === today
|
|
}).length
|
|
})
|
|
|
|
const completedCount = computed(() => {
|
|
return checkInHistory.value.filter((item: CheckInHistoryItem) =>
|
|
item.status === 'success' || item.status === 'ALLOWED'
|
|
).length
|
|
})
|
|
|
|
const pendingCount = computed(() => {
|
|
return checkInHistory.value.filter((item: CheckInHistoryItem) =>
|
|
item.status === 'pending' || item.status === 'NOT_ALLOWED'
|
|
).length
|
|
})
|
|
|
|
// Check-in composable (didefinisikan dulu, saveSuccessfulScan akan di-set nanti)
|
|
let saveSuccessfulScanRef: ((qrData: string) => void) | null = null
|
|
|
|
const {
|
|
lastCheckInResult,
|
|
processQRCode,
|
|
checkInManual: checkInManualComposable,
|
|
} = useCheckIn({
|
|
showSnackbar,
|
|
saveToHistory,
|
|
saveSuccessfulScan: (qrData: string) => {
|
|
// Delegate ke saveSuccessfulScan dari useQRScanner
|
|
if (saveSuccessfulScanRef) {
|
|
saveSuccessfulScanRef(qrData)
|
|
}
|
|
},
|
|
onCheckInSuccess: (result) => {
|
|
infoMessage.value = result.message
|
|
infoAction.value = result.action
|
|
infoDialog.value = true
|
|
},
|
|
autoCloseDialog,
|
|
})
|
|
|
|
// QR Scanner composable (setelah useCheckIn agar bisa akses processQRCode)
|
|
const {
|
|
isScanning,
|
|
hasCamera,
|
|
cameraChecking,
|
|
cameraReady,
|
|
checkCameraAvailability,
|
|
testCamera,
|
|
startScanning,
|
|
stopScanning,
|
|
saveSuccessfulScan,
|
|
} = useQRScanner(
|
|
(decodedText: string) => {
|
|
// Callback ketika QR berhasil di-scan
|
|
scannedData.value = decodedText
|
|
processQRCode(decodedText)
|
|
// Simpan ke history scanned QR
|
|
saveScannedQRData(decodedText)
|
|
},
|
|
showSnackbar
|
|
)
|
|
|
|
// Set saveSuccessfulScanRef setelah useQRScanner didefinisikan
|
|
saveSuccessfulScanRef = saveSuccessfulScan
|
|
|
|
// History Dialog
|
|
const historyDialog = ref(false);
|
|
const qrHistoryDialog = ref(false);
|
|
|
|
// Generate random patient ID function (untuk digunakan di composable dan quick test buttons)
|
|
const generateRandomPatientId = () => {
|
|
// Generate random 6-digit number
|
|
const randomNum = Math.floor(100000 + Math.random() * 900000);
|
|
return `P-${randomNum}`;
|
|
};
|
|
|
|
// QR Generator composable
|
|
const {
|
|
generatePatientId,
|
|
generateStatus,
|
|
generatedQRData,
|
|
generateRandomId,
|
|
generateQuickQR,
|
|
generateQRCode,
|
|
downloadQR,
|
|
copyQRToClipboard,
|
|
shareQR,
|
|
} = useQRGenerator({
|
|
showSnackbar,
|
|
generateRandomPatientId,
|
|
});
|
|
|
|
const statusOptions = [...QR_STATUS_OPTIONS] as Array<{ title: string; value: string }>;
|
|
|
|
|
|
// Watch tab changes untuk stop scanner saat pindah tab
|
|
watch(tab, (newTab: TabValue) => {
|
|
if (newTab !== 'scan' && isScanning.value) {
|
|
stopScanning();
|
|
}
|
|
// Check camera when switching to scan tab
|
|
if (newTab === 'scan' && !hasCamera.value && !cameraChecking.value) {
|
|
checkCameraAvailability();
|
|
}
|
|
});
|
|
|
|
// Check camera on mount
|
|
onMounted(() => {
|
|
if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
|
|
if (navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function') {
|
|
checkCameraAvailability();
|
|
} else {
|
|
console.warn('MediaDevices API not available. Make sure you are using HTTPS or localhost.');
|
|
hasCamera.value = false;
|
|
cameraChecking.value = false;
|
|
showSnackbar('Warning', 'MediaDevices API tidak tersedia. Pastikan menggunakan HTTPS atau localhost.', 'warning', 'mdi-alert');
|
|
}
|
|
} else {
|
|
hasCamera.value = false;
|
|
cameraChecking.value = false;
|
|
}
|
|
});
|
|
|
|
// Cleanup saat component unmount
|
|
onUnmounted(() => {
|
|
if (isScanning.value) {
|
|
stopScanning();
|
|
}
|
|
// Clear dialog auto-close timer
|
|
if (dialogAutoCloseTimer) {
|
|
clearTimeout(dialogAutoCloseTimer);
|
|
dialogAutoCloseTimer = null;
|
|
}
|
|
});
|
|
|
|
// onDetect sudah dipindahkan ke useCheckIn composable sebagai processQRCode
|
|
|
|
const handleInfoAction = async () => {
|
|
// Clear timer jika dialog ditutup secara manual
|
|
if (dialogAutoCloseTimer) {
|
|
clearTimeout(dialogAutoCloseTimer);
|
|
dialogAutoCloseTimer = null;
|
|
}
|
|
|
|
infoDialog.value = false;
|
|
|
|
// Scanner tetap berjalan setelah dialog ditutup
|
|
// Ini memungkinkan user untuk segera memproses QR code berikutnya
|
|
// tanpa perlu memulai scanner lagi
|
|
};
|
|
|
|
// performCheckIn sudah dipindahkan ke useCheckIn composable
|
|
|
|
const checkInManual = async () => {
|
|
try {
|
|
if (!manualInput.value || !manualInput.value.trim()) {
|
|
showSnackbar('Error', 'Mohon isi nomor antrean atau ID pasien', 'error', 'mdi-alert');
|
|
return;
|
|
}
|
|
|
|
const patientId = manualInput.value.trim();
|
|
|
|
// Validate form if available
|
|
if (manualInputTabRef.value?.form) {
|
|
try {
|
|
const isValid = manualInputTabRef.value.form.validate();
|
|
if (!isValid) {
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// If validate fails, continue anyway
|
|
console.warn('Form validation error:', e);
|
|
}
|
|
}
|
|
|
|
await checkInManualComposable(
|
|
patientId,
|
|
() => {
|
|
// onSuccess callback
|
|
try {
|
|
manualInput.value = '';
|
|
if (manualInputTabRef.value?.form) {
|
|
try {
|
|
manualInputTabRef.value.form.reset();
|
|
} catch (e) {
|
|
// Ignore reset errors
|
|
console.warn('Form reset error:', e);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error in success callback:', e);
|
|
}
|
|
},
|
|
() => {
|
|
// onError callback - tidak perlu melakukan apa-apa karena error sudah ditangani di composable
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error('Error in checkInManual:', error);
|
|
showSnackbar('Error', 'Terjadi kesalahan saat melakukan check-in. Silakan coba lagi.', 'error', 'mdi-alert');
|
|
}
|
|
};
|
|
|
|
// generateQRCode, generateQuickQR, downloadQR, copyQRToClipboard, shareQR sudah dipindahkan ke useQRGenerator composable
|
|
|
|
// History Functions - semua sudah dipindahkan ke useCheckInHistory composable
|
|
|
|
const openHistoryDialog = () => {
|
|
loadHistory();
|
|
historyDialog.value = true;
|
|
};
|
|
|
|
const openQRHistoryDialog = () => {
|
|
loadScannedQRHistory();
|
|
qrHistoryDialog.value = true;
|
|
};
|
|
|
|
const handleDeleteHistoryItem = (index: number) => {
|
|
deleteHistoryItem(index);
|
|
showSnackbar('Berhasil', 'Riwayat berhasil dihapus', 'success', 'mdi-check');
|
|
};
|
|
|
|
const handleClearHistory = () => {
|
|
clearHistory();
|
|
showSnackbar('Berhasil', 'Semua riwayat berhasil dihapus', 'success', 'mdi-check');
|
|
};
|
|
|
|
const handleDeleteQRHistoryItem = (index: number) => {
|
|
deleteQRHistoryItem(index);
|
|
showSnackbar('Berhasil', 'Riwayat QR scan berhasil dihapus', 'success', 'mdi-check');
|
|
};
|
|
|
|
const handleClearQRHistory = () => {
|
|
clearQRHistory();
|
|
showSnackbar('Berhasil', 'Semua riwayat QR scan berhasil dihapus', 'success', 'mdi-check');
|
|
};
|
|
|
|
const useQRData = (qrData: string) => {
|
|
qrHistoryDialog.value = false;
|
|
scannedData.value = qrData;
|
|
processQRCode(qrData);
|
|
};
|
|
|
|
// filteredHistory, filteredQRHistory, dan helper functions sudah dipindahkan ke useCheckInHistory composable
|
|
|
|
// loadSuccessfulScans sudah dipanggil di dalam useQRScanner composable saat initialization
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import '~/assets/scss/checkin/variables';
|
|
@import '~/assets/scss/checkin/components';
|
|
@import '~/assets/scss/checkin/dialogs';
|
|
</style> |