update card, update design system, update layout anjungan
This commit is contained in:
No files matched your search
+111
-601
@@ -1,97 +1,56 @@
|
||||
|
||||
<template>
|
||||
<div class="loket-container">
|
||||
<!-- Compact Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<v-icon size="28" color="white">mdi-hospital-building</v-icon>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Admin Klinik</h1>
|
||||
<p class="page-subtitle">{{ currentDate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon="mdi-hospital-building"
|
||||
title="Admin Klinik"
|
||||
:subtitle="currentDate"
|
||||
:show-add-button="false"
|
||||
theme="secondary"
|
||||
/>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<v-row class="content-grid" dense>
|
||||
<!-- Left Column: Current Patient -->
|
||||
<!-- Left Column: Current Patient & Queue Actions -->
|
||||
<v-col cols="12" md="5">
|
||||
<v-card class="current-patient-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="section-label mb-3">SEDANG DIPROSES</div>
|
||||
|
||||
<div v-if="currentProcessingPatient" class="patient-details">
|
||||
<div class="patient-number mb-2">{{ currentProcessingPatient.noAntrian.split(" |")[0] }}</div>
|
||||
<div class="patient-info-text mb-3">
|
||||
<div>{{ currentProcessingPatient.barcode }}</div>
|
||||
<div>{{ currentProcessingPatient.klinik }} | {{ currentProcessingPatient.pembayaran }}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-grid">
|
||||
<v-btn color="#10b981" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'check-in')">
|
||||
<v-icon start size="20">mdi-check</v-icon>
|
||||
Selesai
|
||||
</v-btn>
|
||||
<v-btn color="#f59e0b" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'terlambat')">
|
||||
<v-icon start size="20">mdi-clock-alert</v-icon>
|
||||
Terlambat
|
||||
</v-btn>
|
||||
<v-btn color="#ef4444" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'pending')">
|
||||
<v-icon start size="20">mdi-pause</v-icon>
|
||||
Pending
|
||||
</v-btn>
|
||||
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showChangeKlinikDialog = true">
|
||||
<v-icon start size="20">mdi-swap-horizontal</v-icon>
|
||||
Ubah Klinik
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
|
||||
<div class="empty-text">Tidak ada pasien yang diproses</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- Current Patient Card -->
|
||||
<CurrentPatientCard
|
||||
:patient="currentProcessingPatient"
|
||||
theme="secondary"
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
/>
|
||||
|
||||
<!-- Queue Actions -->
|
||||
<v-card class="queue-actions-card mt-3" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="section-label mb-3">PANGGIL ANTREAN</div>
|
||||
|
||||
<div class="quota-info mb-3">
|
||||
<div class="quota-item">
|
||||
<span class="quota-label">Kuota</span>
|
||||
<span class="quota-value">150</span>
|
||||
</div>
|
||||
<div class="quota-item">
|
||||
<span class="quota-label">Tersedia</span>
|
||||
<span class="quota-value quota-available">{{ 150 - quotaUsed }}</span>
|
||||
</div>
|
||||
<div class="quota-item full-width">
|
||||
<v-progress-linear :model-value="(quotaUsed / 150) * 100" color="#10b981" height="6" rounded class="mt-1"></v-progress-linear>
|
||||
<span class="quota-used">Terpakai: {{ quotaUsed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="call-buttons">
|
||||
<v-btn color="#10b981" variant="outlined" @click="callNext" :disabled="!nextPatient">1</v-btn>
|
||||
<v-btn color="#3b82f6" variant="outlined" @click="callMultiplePatients(5)">5</v-btn>
|
||||
<v-btn color="#f59e0b" variant="outlined" @click="callMultiplePatients(10)">10</v-btn>
|
||||
<v-btn color="#ef4444" variant="outlined" @click="callMultiplePatients(20)">20</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- Queue Actions Card -->
|
||||
<QueueActionsCard
|
||||
class="mt-3"
|
||||
:total-quota="150"
|
||||
:used-quota="quotaUsed"
|
||||
:has-next="!!nextPatient"
|
||||
@call="handleCall"
|
||||
/>
|
||||
|
||||
<!-- Create Queue Buttons -->
|
||||
<div class="create-buttons mt-3">
|
||||
<v-btn color="#f59e0b" variant="flat" block size="large" class="mb-2" @click="showKlinikDialog = true">
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
block
|
||||
size="large"
|
||||
class="mb-2 create-btn"
|
||||
@click="showKlinikDialog = true"
|
||||
>
|
||||
<v-icon start size="20">mdi-hospital-building</v-icon>
|
||||
Buat Antrean Klinik
|
||||
</v-btn>
|
||||
<v-btn color="#3b82f6" variant="flat" block size="large" @click="openPenunjangDialog()">
|
||||
<v-btn
|
||||
color="secondary-600"
|
||||
variant="flat"
|
||||
block
|
||||
size="large"
|
||||
class="create-btn text-white"
|
||||
@click="openPenunjangDialog()"
|
||||
>
|
||||
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
|
||||
Buat Antrean Penunjang
|
||||
</v-btn>
|
||||
@@ -100,219 +59,67 @@
|
||||
|
||||
<!-- Right Column: Patient Table -->
|
||||
<v-col cols="12" md="7">
|
||||
<v-card class="patient-table-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Table Header with Filters -->
|
||||
<div class="table-header mb-4">
|
||||
<div class="section-label">DATA PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
<v-chip-group v-model="selectedStatus" mandatory class="status-filter">
|
||||
<v-chip value="all" :class="{ 'active-chip': selectedStatus === 'all' }">
|
||||
Semua ({{ allPatients.length }})
|
||||
</v-chip>
|
||||
<v-chip value="diloket" :class="{ 'active-chip': selectedStatus === 'diloket' }">
|
||||
Di Klinik ({{ (diLoketPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="terlambat" :class="{ 'active-chip': selectedStatus === 'terlambat' }">
|
||||
Terlambat ({{ (terlambatPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="pending" :class="{ 'active-chip': selectedStatus === 'pending' }">
|
||||
Pending ({{ (pendingPatients || []).length }})
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Cari barcode, nomor antrian..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="search-field"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="filteredPatients"
|
||||
:search="searchQuery"
|
||||
class="patient-table"
|
||||
:items-per-page="10"
|
||||
density="comfortable"
|
||||
>
|
||||
<template #item.noAntrian="{ item }">
|
||||
<div class="queue-number">{{ item.noAntrian.split(" |")[0] }}</div>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
class="status-chip"
|
||||
>
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.klinik="{ item }">
|
||||
<v-chip size="small" variant="outlined" class="klinik-chip">
|
||||
{{ item.klinik }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.aksi="{ item }">
|
||||
<div class="action-buttons-table">
|
||||
<v-btn
|
||||
v-if="item.status === 'diloket'"
|
||||
size="small"
|
||||
color="#10b981"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'terlambat'"
|
||||
size="small"
|
||||
color="#10b981"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'aktifkan')"
|
||||
>
|
||||
Aktifkan
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'pending'"
|
||||
size="small"
|
||||
color="#10b981"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<PatientDataTable
|
||||
:items="allPatients"
|
||||
v-model:selected-status="selectedStatus"
|
||||
v-model:search-query="searchQuery"
|
||||
:di-loket-count="(diLoketPatients || []).length"
|
||||
:terlambat-count="(terlambatPatients || []).length"
|
||||
:pending-count="(pendingPatients || []).length"
|
||||
:status-labels="statusLabels"
|
||||
@action="handleTableAction"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Klinik Dialog -->
|
||||
<v-dialog v-model="showKlinikDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showKlinikDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Pilih Klinik</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="klinikSearch"
|
||||
placeholder="Cari klinik..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="klinik in filteredKliniks" :key="klinik.id" cols="6" sm="4">
|
||||
<v-card class="klinik-card" @click="selectKlinik(klinik)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="klinik-name">{{ klinik.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showKlinikDialog"
|
||||
title="Pilih Klinik"
|
||||
:items="filteredKliniks"
|
||||
v-model:search-query="klinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="selectKlinik"
|
||||
/>
|
||||
|
||||
<!-- Penunjang Dialog -->
|
||||
<v-dialog v-model="showPenunjangDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showPenunjangDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Pilih Penunjang</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="penunjangSearch"
|
||||
placeholder="Cari penunjang..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="penunjang in filteredPenunjangs" :key="penunjang.id" cols="6" sm="4">
|
||||
<v-card class="penunjang-card" @click="selectPenunjang(penunjang)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="penunjang-name">{{ penunjang.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showPenunjangDialog"
|
||||
title="Pilih Penunjang"
|
||||
:items="filteredPenunjangs"
|
||||
v-model:search-query="penunjangSearch"
|
||||
search-placeholder="Cari penunjang..."
|
||||
@select="selectPenunjang"
|
||||
/>
|
||||
|
||||
<!-- Change Klinik Dialog -->
|
||||
<v-dialog v-model="showChangeKlinikDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showChangeKlinikDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Ubah Klinik</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="changeKlinikSearch"
|
||||
placeholder="Cari klinik..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="klinik in filteredChangeKliniks" :key="klinik.id" cols="6" sm="4">
|
||||
<v-card class="klinik-card" @click="changeKlinik(klinik)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="klinik-name">{{ klinik.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showChangeKlinikDialog"
|
||||
title="Ubah Klinik"
|
||||
:items="filteredChangeKliniks"
|
||||
v-model:search-query="changeKlinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="changeKlinik"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
|
||||
{{ snackbarText }}
|
||||
<template v-slot:actions>
|
||||
<v-btn icon size="small" @click="snackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
<AppSnackbar
|
||||
v-model="snackbar"
|
||||
:message="snackbarText"
|
||||
:color="snackbarColor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useQueue } from "../composables/useQueue";
|
||||
import { useQueue } from "@/composables/useQueue";
|
||||
import PageHeader from "@/components/common/PageHeader.vue";
|
||||
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
||||
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
||||
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
|
||||
import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
||||
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
||||
|
||||
const {
|
||||
snackbar,
|
||||
@@ -354,15 +161,21 @@ const currentDate = ref(
|
||||
const selectedStatus = ref("all");
|
||||
const searchQuery = ref("");
|
||||
|
||||
// Combine all patients with status - Fixed to prevent infinite loop
|
||||
// Custom status labels for Klinik
|
||||
const statusLabels = {
|
||||
all: 'Semua',
|
||||
diloket: 'Di Klinik',
|
||||
terlambat: 'Terlambat',
|
||||
pending: 'Pending'
|
||||
};
|
||||
|
||||
// Combine all patients with status
|
||||
const allPatients = computed(() => {
|
||||
try {
|
||||
// Safely access reactive values
|
||||
const diLoketList = diLoketPatients.value || [];
|
||||
const terlambatList = terlambatPatients.value || [];
|
||||
const pendingList = pendingPatients.value || [];
|
||||
|
||||
// Map with status
|
||||
const diLoket = diLoketList.map(p => ({ ...p, status: 'diloket' }));
|
||||
const terlambat = terlambatList.map(p => ({ ...p, status: 'terlambat' }));
|
||||
const pending = pendingList.map(p => ({ ...p, status: 'pending' }));
|
||||
@@ -374,90 +187,32 @@ const allPatients = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Filter patients based on selected status
|
||||
const filteredPatients = computed(() => {
|
||||
try {
|
||||
const all = allPatients.value;
|
||||
if (selectedStatus.value === "all") return all;
|
||||
return all.filter(p => p.status === selectedStatus.value);
|
||||
} catch (error) {
|
||||
console.error('Error in filteredPatients computed:', error);
|
||||
return [];
|
||||
const handlePatientAction = (action) => {
|
||||
if (currentProcessingPatient.value) {
|
||||
processPatient(currentProcessingPatient.value, action);
|
||||
}
|
||||
});
|
||||
|
||||
const tableHeaders = ref([
|
||||
{ title: "No", value: "no", width: "60px", sortable: false },
|
||||
{ title: "Jam Panggil", value: "jamPanggil", width: "100px" },
|
||||
{ title: "Barcode", value: "barcode", width: "130px" },
|
||||
{ title: "No Antrian", value: "noAntrian", width: "140px" },
|
||||
{ title: "Klinik", value: "klinik", width: "100px" },
|
||||
{ title: "Fast Track", value: "fastTrack", width: "100px" },
|
||||
{ title: "Pembayaran", value: "pembayaran", width: "100px" },
|
||||
{ title: "Status", value: "status", width: "100px" },
|
||||
{ title: "Aksi", value: "aksi", width: "100px", sortable: false },
|
||||
]);
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
diloket: "#3b82f6",
|
||||
terlambat: "#f59e0b",
|
||||
pending: "#ef4444"
|
||||
};
|
||||
return colors[status] || "#6b7280";
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
diloket: "Di Klinik",
|
||||
terlambat: "Terlambat",
|
||||
pending: "Pending"
|
||||
};
|
||||
return labels[status] || status;
|
||||
const handleCall = (count) => {
|
||||
if (count === 1) {
|
||||
callNext();
|
||||
} else {
|
||||
callMultiplePatients(count);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableAction = (item, action) => {
|
||||
processPatient(item, action);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.loket-container {
|
||||
background: #f8fafc;
|
||||
background: var(--color-neutral-300);
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
margin: 4px 0 0 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
margin: 0 -8px;
|
||||
}
|
||||
@@ -466,260 +221,15 @@ const getStatusLabel = (status) => {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.current-patient-card,
|
||||
.queue-actions-card,
|
||||
.patient-table-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.patient-details {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.patient-number {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.patient-info-text {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quota-item {
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quota-item.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.quota-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quota-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.quota-available {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.quota-used {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.call-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.call-buttons .v-btn {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.create-buttons .v-btn {
|
||||
.create-buttons .create-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-filter .v-chip {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
height: 32px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.status-filter .v-chip.active-chip {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
:deep(.patient-table) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.patient-table .v-data-table__thead) {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
:deep(.patient-table th) {
|
||||
font-size: 11px !important;
|
||||
font-weight: 700 !important;
|
||||
color: #64748b !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 12px 8px !important;
|
||||
}
|
||||
|
||||
:deep(.patient-table td) {
|
||||
font-size: 13px !important;
|
||||
padding: 10px 8px !important;
|
||||
border-bottom: 1px solid #f1f5f9 !important;
|
||||
}
|
||||
|
||||
:deep(.patient-table tbody tr:hover) {
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
.queue-number {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.klinik-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-buttons-table .v-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
background: #f8fafc;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.klinik-card,
|
||||
.penunjang-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.klinik-card:hover,
|
||||
.penunjang-card:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.klinik-name,
|
||||
.penunjang-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: none !important;
|
||||
letter-spacing: 0 !important;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.loket-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.call-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+97
-596
@@ -1,96 +1,55 @@
|
||||
<template>
|
||||
<div class="loket-container">
|
||||
<!-- Compact Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<v-icon size="28" color="white">mdi-view-dashboard</v-icon>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">Admin Loket</h1>
|
||||
<p class="page-subtitle">{{ currentDate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader
|
||||
icon="mdi-view-dashboard"
|
||||
title="Admin Loket"
|
||||
:subtitle="currentDate"
|
||||
:show-add-button="false"
|
||||
theme="primary"
|
||||
/>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<v-row class="content-grid" dense>
|
||||
<!-- Left Column: Current Patient -->
|
||||
<!-- Left Column: Current Patient & Queue Actions -->
|
||||
<v-col cols="12" md="5">
|
||||
<v-card class="current-patient-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="section-label mb-3">SEDANG DIPROSES</div>
|
||||
|
||||
<div v-if="currentProcessingPatient" class="patient-details">
|
||||
<div class="patient-number mb-2">{{ currentProcessingPatient.noAntrian.split(" |")[0] }}</div>
|
||||
<div class="patient-info-text mb-3">
|
||||
<div>{{ currentProcessingPatient.barcode }}</div>
|
||||
<div>{{ currentProcessingPatient.klinik }} | {{ currentProcessingPatient.pembayaran }}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-grid">
|
||||
<v-btn color="success-600" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'check-in')">
|
||||
<v-icon start size="20">mdi-check</v-icon>
|
||||
Selesai
|
||||
</v-btn>
|
||||
<v-btn color="primary-600" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'terlambat')">
|
||||
<v-icon start size="20">mdi-clock-alert</v-icon>
|
||||
Terlambat
|
||||
</v-btn>
|
||||
<v-btn color="danger-600" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'pending')">
|
||||
<v-icon start size="20">mdi-pause</v-icon>
|
||||
Pending
|
||||
</v-btn>
|
||||
<v-btn color="secondary-600" variant="flat" block size="large" @click="showChangeKlinikDialog = true">
|
||||
<v-icon start size="20">mdi-swap-horizontal</v-icon>
|
||||
Ubah Klinik
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
|
||||
<div class="empty-text">Tidak ada pasien yang diproses</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- Current Patient Card -->
|
||||
<CurrentPatientCard
|
||||
:patient="currentProcessingPatient"
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
/>
|
||||
|
||||
<!-- Queue Actions -->
|
||||
<v-card class="queue-actions-card mt-3" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="section-label mb-3">PANGGIL ANTREAN</div>
|
||||
|
||||
<div class="quota-info mb-3">
|
||||
<div class="quota-item">
|
||||
<span class="quota-label">Kuota</span>
|
||||
<span class="quota-value">150</span>
|
||||
</div>
|
||||
<div class="quota-item">
|
||||
<span class="quota-label">Tersedia</span>
|
||||
<span class="quota-value quota-available">{{ 150 - quotaUsed }}</span>
|
||||
</div>
|
||||
<div class="quota-item full-width">
|
||||
<v-progress-linear :model-value="(quotaUsed / 150) * 100" color="success-600" height="6" rounded class="mt-1"></v-progress-linear>
|
||||
<span class="quota-used">Terpakai: {{ quotaUsed }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="call-buttons">
|
||||
<v-btn color="success-600" variant="outlined" @click="callNext" :disabled="!nextPatient">1</v-btn>
|
||||
<v-btn color="secondary-600" variant="outlined" @click="callMultiplePatients(5)">5</v-btn>
|
||||
<v-btn color="primary-600" variant="outlined" @click="callMultiplePatients(10)">10</v-btn>
|
||||
<v-btn color="danger-600" variant="outlined" @click="callMultiplePatients(20)">20</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- Queue Actions Card -->
|
||||
<QueueActionsCard
|
||||
class="mt-3"
|
||||
:total-quota="150"
|
||||
:used-quota="quotaUsed"
|
||||
:has-next="!!nextPatient"
|
||||
@call="handleCall"
|
||||
/>
|
||||
|
||||
<!-- Create Queue Buttons -->
|
||||
<div class="create-buttons mt-3">
|
||||
<v-btn color="primary-600" variant="flat" block size="large" class="mb-2" @click="showKlinikDialog = true">
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
block
|
||||
size="large"
|
||||
class="mb-2"
|
||||
@click="showKlinikDialog = true"
|
||||
>
|
||||
<v-icon start size="20">mdi-hospital-building</v-icon>
|
||||
Buat Antrean Klinik
|
||||
</v-btn>
|
||||
<v-btn color="secondary-600" variant="flat" block size="large" @click="openPenunjangDialog()">
|
||||
<v-btn
|
||||
color="secondary-600"
|
||||
variant="flat"
|
||||
block
|
||||
size="large"
|
||||
class="text-white"
|
||||
@click="openPenunjangDialog()"
|
||||
>
|
||||
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
|
||||
Buat Antrean Penunjang
|
||||
</v-btn>
|
||||
@@ -99,219 +58,66 @@
|
||||
|
||||
<!-- Right Column: Patient Table -->
|
||||
<v-col cols="12" md="7">
|
||||
<v-card class="patient-table-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Table Header with Filters -->
|
||||
<div class="table-header mb-4">
|
||||
<div class="section-label">DATA PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
<v-chip-group v-model="selectedStatus" mandatory class="status-filter">
|
||||
<v-chip value="all" :class="{ 'active-chip': selectedStatus === 'all' }">
|
||||
Semua ({{ allPatients.length }})
|
||||
</v-chip>
|
||||
<v-chip value="diloket" :class="{ 'active-chip': selectedStatus === 'diloket' }">
|
||||
Di Loket ({{ (diLoketPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="terlambat" :class="{ 'active-chip': selectedStatus === 'terlambat' }">
|
||||
Terlambat ({{ (terlambatPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="pending" :class="{ 'active-chip': selectedStatus === 'pending' }">
|
||||
Pending ({{ (pendingPatients || []).length }})
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Cari barcode, nomor antrian..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="search-field"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="filteredPatients"
|
||||
:search="searchQuery"
|
||||
class="patient-table"
|
||||
:items-per-page="10"
|
||||
density="comfortable"
|
||||
>
|
||||
<template #item.noAntrian="{ item }">
|
||||
<div class="queue-number">{{ item.noAntrian.split(" |")[0] }}</div>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
class="status-chip"
|
||||
>
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.klinik="{ item }">
|
||||
<v-chip size="small" variant="outlined" class="klinik-chip">
|
||||
{{ item.klinik }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.aksi="{ item }">
|
||||
<div class="action-buttons-table">
|
||||
<v-btn
|
||||
v-if="item.status === 'diloket'"
|
||||
size="small"
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'terlambat'"
|
||||
size="small"
|
||||
color="success-600"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'aktifkan')"
|
||||
>
|
||||
Aktifkan
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'pending'"
|
||||
size="small"
|
||||
color="success-600"
|
||||
variant="flat"
|
||||
@click="processPatient(item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<PatientDataTable
|
||||
:items="allPatients"
|
||||
v-model:selected-status="selectedStatus"
|
||||
v-model:search-query="searchQuery"
|
||||
:di-loket-count="(diLoketPatients || []).length"
|
||||
:terlambat-count="(terlambatPatients || []).length"
|
||||
:pending-count="(pendingPatients || []).length"
|
||||
@action="handleTableAction"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Klinik Dialog -->
|
||||
<v-dialog v-model="showKlinikDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showKlinikDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Pilih Klinik</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="klinikSearch"
|
||||
placeholder="Cari klinik..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="klinik in filteredKliniks" :key="klinik.id" cols="6" sm="4">
|
||||
<v-card class="klinik-card" @click="selectKlinik(klinik)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="klinik-name">{{ klinik.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showKlinikDialog"
|
||||
title="Pilih Klinik"
|
||||
:items="filteredKliniks"
|
||||
v-model:search-query="klinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="selectKlinik"
|
||||
/>
|
||||
|
||||
<!-- Penunjang Dialog -->
|
||||
<v-dialog v-model="showPenunjangDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showPenunjangDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Pilih Penunjang</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="penunjangSearch"
|
||||
placeholder="Cari penunjang..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="penunjang in filteredPenunjangs" :key="penunjang.id" cols="6" sm="4">
|
||||
<v-card class="penunjang-card" @click="selectPenunjang(penunjang)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="penunjang-name">{{ penunjang.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showPenunjangDialog"
|
||||
title="Pilih Penunjang"
|
||||
:items="filteredPenunjangs"
|
||||
v-model:search-query="penunjangSearch"
|
||||
search-placeholder="Cari penunjang..."
|
||||
@select="selectPenunjang"
|
||||
/>
|
||||
|
||||
<!-- Change Klinik Dialog -->
|
||||
<v-dialog v-model="showChangeKlinikDialog" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-header">
|
||||
<v-btn icon size="small" @click="showChangeKlinikDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<span>Ubah Klinik</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
v-model="changeKlinikSearch"
|
||||
placeholder="Cari klinik..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
></v-text-field>
|
||||
|
||||
<v-row dense>
|
||||
<v-col v-for="klinik in filteredChangeKliniks" :key="klinik.id" cols="6" sm="4">
|
||||
<v-card class="klinik-card" @click="changeKlinik(klinik)" elevation="0">
|
||||
<v-card-text class="text-center pa-3">
|
||||
<div class="klinik-name">{{ klinik.name }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<SelectionDialog
|
||||
v-model="showChangeKlinikDialog"
|
||||
title="Ubah Klinik"
|
||||
:items="filteredChangeKliniks"
|
||||
v-model:search-query="changeKlinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="changeKlinik"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
|
||||
{{ snackbarText }}
|
||||
<template v-slot:actions>
|
||||
<v-btn icon size="small" @click="snackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
<AppSnackbar
|
||||
v-model="snackbar"
|
||||
:message="snackbarText"
|
||||
:color="snackbarColor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useQueue } from "../composables/useQueue";
|
||||
import { useQueue } from "@/composables/useQueue";
|
||||
import PageHeader from "@/components/common/PageHeader.vue";
|
||||
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
||||
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
||||
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
|
||||
import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
||||
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
||||
|
||||
const {
|
||||
snackbar,
|
||||
@@ -359,47 +165,25 @@ const allPatients = computed(() => {
|
||||
const terlambat = (terlambatPatients.value || []).map(p => ({ ...p, status: 'terlambat' }));
|
||||
const pending = (pendingPatients.value || []).map(p => ({ ...p, status: 'pending' }));
|
||||
|
||||
console.log('Di Loket:', diLoket.length);
|
||||
console.log('Terlambat:', terlambat.length);
|
||||
console.log('Pending:', pending.length);
|
||||
|
||||
return [...diLoket, ...terlambat, ...pending];
|
||||
});
|
||||
|
||||
// Filter patients based on selected status
|
||||
const filteredPatients = computed(() => {
|
||||
if (selectedStatus.value === "all") return allPatients.value;
|
||||
return allPatients.value.filter(p => p.status === selectedStatus.value);
|
||||
});
|
||||
|
||||
const tableHeaders = ref([
|
||||
{ title: "No", value: "no", width: "60px", sortable: false },
|
||||
{ title: "Jam Panggil", value: "jamPanggil", width: "100px" },
|
||||
{ title: "Barcode", value: "barcode", width: "130px" },
|
||||
{ title: "No Antrian", value: "noAntrian", width: "140px" },
|
||||
{ title: "Klinik", value: "klinik", width: "100px" },
|
||||
{ title: "Fast Track", value: "fastTrack", width: "100px" },
|
||||
{ title: "Pembayaran", value: "pembayaran", width: "100px" },
|
||||
{ title: "Status", value: "status", width: "100px" },
|
||||
{ title: "Aksi", value: "aksi", width: "100px", sortable: false },
|
||||
]);
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
diloket: "var(--color-secondary-600)",
|
||||
terlambat: "var(--color-primary-600)",
|
||||
pending: "var(--color-danger-600)"
|
||||
};
|
||||
return colors[status] || "var(--color-neutral-600)";
|
||||
const handlePatientAction = (action) => {
|
||||
if (currentProcessingPatient.value) {
|
||||
processPatient(currentProcessingPatient.value, action);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
diloket: "Di Loket",
|
||||
terlambat: "Terlambat",
|
||||
pending: "Pending"
|
||||
};
|
||||
return labels[status] || status;
|
||||
const handleCall = (count) => {
|
||||
if (count === 1) {
|
||||
callNext();
|
||||
} else {
|
||||
callMultiplePatients(count);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableAction = (item, action) => {
|
||||
processPatient(item, action);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -410,42 +194,6 @@ const getStatusLabel = (status) => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 12px rgba(255, 155, 27, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
margin: 4px 0 0 0;
|
||||
opacity: 0.9;
|
||||
color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
margin: 0 -8px;
|
||||
}
|
||||
@@ -454,261 +202,14 @@ const getStatusLabel = (status) => {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.current-patient-card,
|
||||
.queue-actions-card,
|
||||
.patient-table-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--color-neutral-600);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.patient-details {
|
||||
background: linear-gradient(135deg, var(--color-primary-200) 0%, var(--color-primary-300) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.patient-number {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.patient-info-text {
|
||||
font-size: 13px;
|
||||
color: var(--color-neutral-700);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 13px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quota-item {
|
||||
background: var(--color-neutral-300);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quota-item.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.quota-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quota-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.quota-available {
|
||||
color: var(--color-success-600);
|
||||
}
|
||||
|
||||
.quota-used {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.call-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.call-buttons .v-btn {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.create-buttons .v-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-filter .v-chip {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
.status-filter .v-chip.active-chip {
|
||||
background: var(--color-secondary-600);
|
||||
color: var(--color-neutral-100);
|
||||
border-color: var(--color-secondary-600);
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
:deep(.patient-table) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.patient-table .v-data-table__thead) {
|
||||
background: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
:deep(.patient-table th) {
|
||||
font-size: 11px !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--color-neutral-600) !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 12px 8px !important;
|
||||
}
|
||||
|
||||
:deep(.patient-table td) {
|
||||
font-size: 13px !important;
|
||||
padding: 10px 8px !important;
|
||||
border-bottom: 1px solid var(--color-neutral-400) !important;
|
||||
}
|
||||
|
||||
:deep(.patient-table tbody tr:hover) {
|
||||
background: var(--color-neutral-300) !important;
|
||||
}
|
||||
|
||||
.queue-number {
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.klinik-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-buttons-table .v-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
background: var(--color-neutral-300);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.klinik-card,
|
||||
.penunjang-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.klinik-card:hover,
|
||||
.penunjang-card:hover {
|
||||
border-color: var(--color-secondary-600);
|
||||
background: var(--color-secondary-200);
|
||||
}
|
||||
|
||||
.klinik-name,
|
||||
.penunjang-name {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: none !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.loket-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.call-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+559
-217
File diff suppressed because it is too large
Load diff
+739
-231
File diff suppressed because it is too large
Load diff
@@ -22,7 +22,7 @@
|
||||
<!-- Hero Call Section -->
|
||||
<div v-if="currentCalledQueue" class="hero-call-section">
|
||||
<div class="call-label">
|
||||
<v-icon size="32" class="mr-2">mdi-bullhorn</v-icon>
|
||||
<v-icon size="32" color="white" class="mr-2">mdi-bullhorn</v-icon>
|
||||
NOMOR ANTRIAN YANG DIPANGGIL
|
||||
</div>
|
||||
<div class="call-number">{{ currentCalledQueue.noAntrian.split(' |')[0] }}</div>
|
||||
@@ -39,13 +39,9 @@
|
||||
<!-- Clinic Header -->
|
||||
<div class="clinic-header-bar">
|
||||
<span class="clinic-title">{{ ruang.namaKlinik }} - {{ ruang.namaRuang }}</span>
|
||||
<v-chip
|
||||
:color="ruang.totalQueues > 0 ? '#FF9800' : '#89939E'"
|
||||
size="small"
|
||||
class="clinic-count"
|
||||
>
|
||||
<v-icon size="16" class="mr-1">mdi-account-multiple</v-icon>
|
||||
{{ ruang.totalQueues }}
|
||||
<v-chip size="small" class="clinic-count">
|
||||
<v-icon size="16" color="white" class="mr-1">mdi-account-multiple</v-icon>
|
||||
<span class="count-text">{{ ruang.totalQueues }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +71,7 @@
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!ruang.currentQueue && ruang.nextQueues.length === 0" class="empty-state">
|
||||
<v-icon size="56" color="#F5F7FA">mdi-clock-outline</v-icon>
|
||||
<v-icon size="48" color="grey-lighten-3">mdi-clock-outline</v-icon>
|
||||
<p class="empty-text">Tidak Ada Antrian</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,8 +81,8 @@
|
||||
<!-- Footer Stats -->
|
||||
<div class="footer-stats-bar">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon" style="background: #FFF3E0;">
|
||||
<v-icon size="32" color="#FF9800">mdi-format-list-numbered</v-icon>
|
||||
<div class="stat-icon stat-icon-total">
|
||||
<v-icon size="32" color="primary-600">mdi-format-list-numbered</v-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statistics.total }}</div>
|
||||
@@ -97,8 +93,8 @@
|
||||
<div class="stat-divider"></div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon" style="background: #FFF3E0;">
|
||||
<v-icon size="32" color="#FF9800">mdi-clock-alert-outline</v-icon>
|
||||
<div class="stat-icon stat-icon-waiting">
|
||||
<v-icon size="32" color="primary-600">mdi-clock-alert-outline</v-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statistics.waiting }}</div>
|
||||
@@ -109,8 +105,8 @@
|
||||
<div class="stat-divider"></div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon" style="background: #E8F5E9;">
|
||||
<v-icon size="32" color="#18B653">mdi-account-check</v-icon>
|
||||
<div class="stat-icon stat-icon-active">
|
||||
<v-icon size="32" color="success-600">mdi-account-check</v-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statistics.active }}</div>
|
||||
@@ -119,7 +115,7 @@
|
||||
</div>
|
||||
|
||||
<div class="footer-message">
|
||||
<v-icon size="24" color="#FF9800" class="mr-2">mdi-information</v-icon>
|
||||
<v-icon size="24" color="primary-600" class="mr-2">mdi-information</v-icon>
|
||||
<span>Harap perhatikan nomor antrian Anda di layar</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,24 +138,18 @@ const currentTime = ref('')
|
||||
const currentDate = ref('')
|
||||
let timeInterval = null
|
||||
|
||||
// Get patients from klinik stage
|
||||
const klinikPatients = computed(() => {
|
||||
return queueStore.getPatientsByStage('klinik').value.all
|
||||
})
|
||||
|
||||
// Get all ruang from master store
|
||||
const allRuangList = computed(() => {
|
||||
return masterStore.getAllRuangList || []
|
||||
})
|
||||
|
||||
// Display ruang with queue data
|
||||
const displayedRuang = computed(() => {
|
||||
// Ambil max 9 ruang pertama untuk display
|
||||
const ruangToDisplay = (allRuangList.value || []).slice(0, 9)
|
||||
|
||||
return ruangToDisplay.map(ruang => {
|
||||
// Filter pasien berdasarkan klinik dan ruang (jika ada field ruang di patient data)
|
||||
// Untuk sementara, kita filter berdasarkan klinik saja
|
||||
const queues = klinikPatients.value
|
||||
.filter(p => p.klinik === ruang.namaKlinik)
|
||||
.sort((a, b) => {
|
||||
@@ -205,7 +195,6 @@ const currentCalledQueue = computed(() => {
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
|
||||
if (calledQueues.length > 0) {
|
||||
// Find ruang for this patient
|
||||
const patient = calledQueues[0]
|
||||
const ruang = (allRuangList.value || []).find(r => r.namaKlinik === patient.klinik)
|
||||
return {
|
||||
@@ -252,61 +241,41 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Colors from Design System
|
||||
$neutral-900: #212121;
|
||||
$neutral-800: #4D4D4D;
|
||||
$neutral-700: #717171;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-300: #F5F7FA;
|
||||
$neutral-100: #FFFFFF;
|
||||
|
||||
$primary-600: #FFA532;
|
||||
|
||||
$success-600: #009262;
|
||||
|
||||
$danger-700: #E01507;
|
||||
$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;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
// Apply font family
|
||||
* {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.antrian-display-container {
|
||||
background: $neutral-100;
|
||||
background: var(--color-neutral-300);
|
||||
min-height: 100vh;
|
||||
max-width: 1920px;
|
||||
max-height: 1080px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding: 24px;
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HEADER
|
||||
// ============================================
|
||||
/* ========== HEADER ========== */
|
||||
.display-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
||||
border-radius: 16px;
|
||||
padding: 24px 40px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 24px rgba(255, 152, 0, 0.2);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 24px rgba(255, 155, 27, 0.3);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
@@ -317,122 +286,105 @@ $font-weight-bold: 700;
|
||||
}
|
||||
|
||||
.hospital-name {
|
||||
font-size: 42px;
|
||||
line-height: 1.2;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-100);
|
||||
margin: 0;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.display-subtitle {
|
||||
font-size: 22px;
|
||||
line-height: 1.3;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $neutral-100;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-100);
|
||||
margin: 6px 0 0 0;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.datetime-display {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time-large {
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-weight: 900;
|
||||
color: var(--color-neutral-100);
|
||||
letter-spacing: 2px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.date-small {
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
color: $neutral-100;
|
||||
color: var(--color-neutral-100);
|
||||
margin-top: 8px;
|
||||
font-weight: $font-weight-regular;
|
||||
font-weight: 500;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HERO CALL SECTION
|
||||
// ============================================
|
||||
/* ========== HERO CALL SECTION ========== */
|
||||
.hero-call-section {
|
||||
background: linear-gradient(135deg, $danger-700 0%, $danger-600 100%);
|
||||
background: linear-gradient(135deg, var(--color-danger-600) 0%, var(--color-danger-700) 100%);
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.25);
|
||||
animation: pulse-shadow 2s infinite;
|
||||
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.35);
|
||||
}
|
||||
|
||||
.call-label {
|
||||
font-size: 28px;
|
||||
line-height: 1.3;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-100);
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.call-number {
|
||||
font-size: 110px;
|
||||
line-height: 1;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-size: 140px;
|
||||
font-weight: 900;
|
||||
color: var(--color-neutral-100);
|
||||
margin: 16px 0;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 8px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.call-clinic {
|
||||
font-size: 38px;
|
||||
line-height: 1.2;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-100);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
@keyframes pulse-shadow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 12px 32px rgba(224, 21, 7, 0.25);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 16px 48px rgba(224, 21, 7, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CLINICS GRID
|
||||
// ============================================
|
||||
/* ========== CLINICS GRID ========== */
|
||||
.clinics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clinic-box {
|
||||
background: $neutral-100;
|
||||
border: 2px solid #FFF3E0;
|
||||
background: var(--color-neutral-100);
|
||||
border: 2px solid var(--color-primary-200);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(255, 152, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 260px;
|
||||
box-shadow: 0 4px 12px rgba(255, 155, 27, 0.12);
|
||||
min-height: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clinic-box:hover {
|
||||
box-shadow: 0 8px 24px rgba(255, 152, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.clinic-header-bar {
|
||||
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
|
||||
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -441,20 +393,27 @@ $font-weight-bold: 700;
|
||||
|
||||
.clinic-title {
|
||||
font-size: 22px;
|
||||
line-height: 1.3;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-100;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-100);
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clinic-count {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: $neutral-100 !important;
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-neutral-100) !important;
|
||||
font-weight: 700;
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.count-text {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.clinic-content {
|
||||
@@ -462,43 +421,44 @@ $font-weight-bold: 700;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: #FAFBFC;
|
||||
padding: 20px;
|
||||
background: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.current-section {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.current-label {
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $success-600;
|
||||
font-weight: 700;
|
||||
color: var(--color-success-600);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.current-number {
|
||||
font-size: 68px;
|
||||
font-size: 80px;
|
||||
font-weight: 900;
|
||||
color: var(--color-neutral-900);
|
||||
letter-spacing: 4px;
|
||||
line-height: 1;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-900;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.next-section {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.next-label {
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-700;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-700);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -510,15 +470,16 @@ $font-weight-bold: 700;
|
||||
}
|
||||
|
||||
.next-item {
|
||||
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
|
||||
color: $neutral-100;
|
||||
padding: 8px 18px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-secondary-600);
|
||||
color: var(--color-neutral-100);
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
font-size: 22px;
|
||||
line-height: 1.3;
|
||||
font-weight: $font-weight-bold;
|
||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.2);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(6, 99, 199, 0.25);
|
||||
letter-spacing: 1px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -527,25 +488,22 @@ $font-weight-bold: 700;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
color: $neutral-600;
|
||||
font-size: 16px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-top: 12px;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FOOTER STATS
|
||||
// ============================================
|
||||
/* ========== FOOTER STATS ========== */
|
||||
.footer-stats-bar {
|
||||
background: $neutral-100;
|
||||
border: 2px solid #FFF3E0;
|
||||
border-radius: 20px;
|
||||
padding: 24px 40px;
|
||||
background: var(--color-neutral-100);
|
||||
border: 2px solid var(--color-primary-200);
|
||||
border-radius: 16px;
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
box-shadow: 0 4px 16px rgba(255, 152, 0, 0.08);
|
||||
box-shadow: 0 4px 12px rgba(255, 155, 27, 0.12);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
@@ -563,36 +521,232 @@ $font-weight-bold: 700;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon-total,
|
||||
.stat-icon-waiting {
|
||||
background: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.stat-icon-active {
|
||||
background: var(--color-success-200);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 42px;
|
||||
font-size: 40px;
|
||||
font-weight: 900;
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $neutral-900;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 15px;
|
||||
line-height: 1.3;
|
||||
color: $neutral-700;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 14px;
|
||||
color: var(--color-neutral-700);
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2px;
|
||||
height: 64px;
|
||||
background: #FFF3E0;
|
||||
height: 60px;
|
||||
background: var(--color-primary-200);
|
||||
}
|
||||
|
||||
.footer-message {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 20px;
|
||||
line-height: 1.3;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $neutral-800;
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-800);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ========== RESPONSIVE ========== */
|
||||
@media (min-width: 1920px) and (min-height: 1080px) {
|
||||
.antrian-display-container {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.antrian-display-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.display-header {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.logo-circle .v-icon {
|
||||
font-size: 40px !important;
|
||||
}
|
||||
|
||||
.hospital-name {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.display-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.time-large {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.date-small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hero-call-section {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.call-label {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.call-number {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.call-clinic {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.clinics-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.clinic-box {
|
||||
min-height: 220px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.clinic-header-bar {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.clinic-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.clinic-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.current-number {
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
.next-item {
|
||||
font-size: 20px;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
|
||||
.footer-stats-bar {
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.stat-icon .v-icon {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.footer-message {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.display-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.datetime-display {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clinics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.footer-stats-bar {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer-message {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.clinics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hospital-name {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.time-large {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.call-number {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.current-number {
|
||||
font-size: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent text selection */
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
+557
-397
File diff suppressed because it is too large
Load diff
+208
-140
@@ -3,12 +3,12 @@
|
||||
|
||||
<div class="d-flex justify-space-between align-center mb-10">
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-black primary-text">
|
||||
<h1 class="dashboard-title">
|
||||
Antrean Dashboard
|
||||
</h1>
|
||||
<p v-if="user" class="text-subtitle-1 secondary-text mt-2">
|
||||
<p v-if="user" class="dashboard-subtitle">
|
||||
Selamat Datang,
|
||||
<span class="font-weight-bold primary-accent">
|
||||
<span class="user-name">
|
||||
{{ user.name || user.preferred_username }}
|
||||
</span>! 🍊
|
||||
</p>
|
||||
@@ -19,9 +19,9 @@
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
color="#FF9B1B"
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
class="text-caption font-weight-bold rounded-lg elevation-2 mr-4 export-btn"
|
||||
class="export-btn mr-4"
|
||||
>
|
||||
<v-icon start icon="mdi-download-box-outline"></v-icon>
|
||||
Export Data ({{ exportType }})
|
||||
@@ -42,8 +42,8 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-chip color="white" variant="flat" class="pa-4 font-weight-bold text-caption secondary-text rounded-lg date-chip">
|
||||
<v-icon start icon="mdi-calendar-check-outline" class="primary-accent"></v-icon>
|
||||
<v-chip color="white" variant="flat" class="date-chip">
|
||||
<v-icon start icon="mdi-calendar-check-outline" color="primary-600"></v-icon>
|
||||
{{ currentDate }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -51,48 +51,48 @@
|
||||
|
||||
<v-row class="mb-10">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
||||
<v-card class="stat-card stat-card-primary">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="48" class="mr-4 primary-accent">mdi-tablet-dashboard</v-icon>
|
||||
<v-icon size="48" color="primary-600" class="mr-4">mdi-tablet-dashboard</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black primary-accent">150</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean Online</div>
|
||||
<div class="stat-value">150</div>
|
||||
<div class="stat-label">Total Antrean Online</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
||||
<v-card class="stat-card stat-card-secondary">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="48" class="mr-4 secondary-accent">mdi-account-hard-hat</v-icon>
|
||||
<v-icon size="48" color="secondary-600" class="mr-4">mdi-account-hard-hat</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black secondary-accent">100</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean MJKN</div>
|
||||
<div class="stat-value">100</div>
|
||||
<div class="stat-label">Total Antrean MJKN</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
||||
<v-card class="stat-card stat-card-primary">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="48" class="mr-4 primary-accent">mdi-account-group</v-icon>
|
||||
<v-icon size="48" color="primary-600" class="mr-4">mdi-account-group</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black primary-accent">500</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Kunjungan</div>
|
||||
<div class="stat-value">500</div>
|
||||
<div class="stat-label">Total Kunjungan</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
||||
<v-card class="stat-card stat-card-secondary">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="48" class="mr-4 secondary-accent">mdi-timer-sand</v-icon>
|
||||
<v-icon size="48" color="secondary-600" class="mr-4">mdi-timer-sand</v-icon>
|
||||
<div>
|
||||
<div class="text-h4 font-weight-black secondary-accent">7.5s</div>
|
||||
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Avg. Check-in Time</div>
|
||||
<div class="stat-value">7.5s</div>
|
||||
<div class="stat-label">Avg. Check-in Time</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
@@ -101,10 +101,10 @@
|
||||
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
||||
<span class="text-h6 font-weight-bold primary-text">Registration Trend</span>
|
||||
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="#D17A4A" class="text-caption rounded-lg">
|
||||
<v-card class="chart-card">
|
||||
<v-card-title class="chart-header">
|
||||
<span class="chart-title">Registration Trend</span>
|
||||
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="primary-600" class="period-toggle">
|
||||
<v-btn size="small" value="day">Day</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-card-title>
|
||||
@@ -119,24 +119,24 @@
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
||||
<span class="text-h6 font-weight-bold primary-text">Total Registrasi Perbulan</span>
|
||||
<v-card class="chart-card">
|
||||
<v-card-title class="chart-header">
|
||||
<span class="chart-title">Total Registrasi Perbulan</span>
|
||||
<div>
|
||||
<v-chip
|
||||
:color="activeYear === '2024' ? '#D17A4A' : '#F5F5F5'"
|
||||
:text-color="activeYear === '2024' ? 'white' : '#FF9B1B'"
|
||||
:color="activeYear === '2024' ? 'primary-600' : 'neutral-300'"
|
||||
:text-color="activeYear === '2024' ? 'white' : 'neutral-700'"
|
||||
variant="flat"
|
||||
class="mr-2 text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
||||
class="year-chip mr-2"
|
||||
@click="changeYear('2024')"
|
||||
>
|
||||
2024
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="activeYear === '2025' ? '#D17A4A' : '#F5F5F5'"
|
||||
:text-color="activeYear === '2025' ? 'white' : '#FF9B1B'"
|
||||
:color="activeYear === '2025' ? 'primary-600' : 'neutral-300'"
|
||||
:text-color="activeYear === '2025' ? 'white' : 'neutral-700'"
|
||||
variant="flat"
|
||||
class="text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
||||
class="year-chip"
|
||||
@click="changeYear('2025')"
|
||||
>
|
||||
2025
|
||||
@@ -156,10 +156,10 @@
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">
|
||||
Realtime Check-in Velocity
|
||||
<v-chip size="small" color="success" class="ml-2">
|
||||
<v-card class="chart-card">
|
||||
<v-card-title class="chart-header">
|
||||
<span class="chart-title">Realtime Check-in Velocity</span>
|
||||
<v-chip size="small" color="success-600" class="live-chip">
|
||||
<v-icon start size="small">mdi-circle</v-icon>
|
||||
Live
|
||||
</v-chip>
|
||||
@@ -175,8 +175,10 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
||||
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">Registrant Type Breakdown</v-card-title>
|
||||
<v-card class="chart-card">
|
||||
<v-card-title class="chart-header">
|
||||
<span class="chart-title">Registrant Type Breakdown</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="d-flex justify-center align-center pa-0" style="height: 350px;">
|
||||
<Pie
|
||||
:data="pieData"
|
||||
@@ -188,9 +190,9 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-overlay v-model="isLoading" contained class="align-center justify-center dashboard-bg">
|
||||
<v-progress-circular indeterminate size="64" color="#D17A4A"></v-progress-circular>
|
||||
<p class="mt-4 primary-text">Loading dashboard...</p>
|
||||
<v-overlay v-model="isLoading" contained class="align-center justify-center">
|
||||
<v-progress-circular indeterminate size="64" color="primary-600"></v-progress-circular>
|
||||
<p class="loading-text">Loading dashboard...</p>
|
||||
</v-overlay>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -226,17 +228,11 @@ definePageMeta({
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, PointElement, LineElement);
|
||||
|
||||
// --- KEYCLOAK/AUTH INTEGRATION ---
|
||||
// const { user, isLoading, checkAuth, logout } = useAuth()
|
||||
// const navigateTo = (path) => { console.log('Navigating to', path); };
|
||||
// Mock user for demo purposes - replace with your actual auth
|
||||
const user = ref({ name: 'Admin User', preferred_username: 'admin' });
|
||||
const isLoading = ref(false);
|
||||
const checkAuth = async () => user.value;
|
||||
const navigateTo = (path) => console.log('Navigate to:', path);
|
||||
// ---------------------------------
|
||||
|
||||
// --- EXPORT STATE ---
|
||||
const exportType = ref('JSON');
|
||||
const exportOptions = ref([
|
||||
{ title: 'JSON (.json)', type: 'json', icon: 'mdi-code-json' },
|
||||
@@ -244,14 +240,11 @@ const exportOptions = ref([
|
||||
{ title: 'Excel (.xlsx) - Server Only', type: 'excel', icon: 'mdi-file-excel' },
|
||||
{ title: 'PDF (.pdf) - Placeholder', type: 'pdf', icon: 'mdi-file-pdf' },
|
||||
]);
|
||||
// --------------------
|
||||
|
||||
// Dashboard state
|
||||
const currentDate = ref('');
|
||||
const activeYear = ref('2025');
|
||||
const queueTimePeriod = ref('day');
|
||||
|
||||
// --- MOCK DATA FOR CHARTS ---
|
||||
const mockQueueData = ref([
|
||||
{ date: '2025-10-13', registrants: 300, attendees: 250 },
|
||||
{ date: '2025-10-14', registrants: 450, attendees: 400 },
|
||||
@@ -261,7 +254,6 @@ const mockQueueData = ref([
|
||||
{ date: '2025-10-18', registrants: 435, attendees: 380 },
|
||||
]);
|
||||
|
||||
// --- REALTIME CHART LOGIC - FIXED ---
|
||||
const realtimeChart = ref(null);
|
||||
const maxDataPoints = 10;
|
||||
let realtimeInterval = null;
|
||||
@@ -273,15 +265,15 @@ const realtimeLineData = ref({
|
||||
datasets: [
|
||||
{
|
||||
label: 'Check-ins/min',
|
||||
backgroundColor: 'rgba(74, 95, 122, 0.15)',
|
||||
borderColor: '#5FB7FF',
|
||||
backgroundColor: 'rgba(6, 99, 199, 0.1)',
|
||||
borderColor: '#0671E0',
|
||||
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 20) + 40),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
pointBackgroundColor: '#0663C7',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -289,7 +281,6 @@ const realtimeLineData = ref({
|
||||
const lineOptions = ref({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 300 },
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
title: { display: false }
|
||||
@@ -312,32 +303,22 @@ const updateRealtimeData = () => {
|
||||
const data = realtimeLineData.value.datasets[0].data;
|
||||
const labels = realtimeLineData.value.labels;
|
||||
|
||||
// Remove first data point
|
||||
data.shift();
|
||||
labels.shift();
|
||||
|
||||
// Add new data point
|
||||
const newDataPoint = Math.floor(Math.random() * 20) + 40;
|
||||
const newTimeLabel = dayjs().format('HH:mm:ss');
|
||||
|
||||
data.push(newDataPoint);
|
||||
labels.push(newTimeLabel);
|
||||
|
||||
console.log('Realtime chart updated:', newTimeLabel, newDataPoint);
|
||||
};
|
||||
// --- END REALTIME CHART LOGIC ---
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const sessionUser = await checkAuth()
|
||||
if (sessionUser) {
|
||||
// Set date with Indonesian locale
|
||||
currentDate.value = dayjs().format('dddd, DD MMMM YYYY');
|
||||
console.log('Current date set:', currentDate.value);
|
||||
|
||||
// Start realtime updates every 5 seconds
|
||||
realtimeInterval = setInterval(updateRealtimeData, 5000);
|
||||
console.log('Realtime interval started');
|
||||
} else {
|
||||
await navigateTo('/LoginPage');
|
||||
}
|
||||
@@ -350,7 +331,6 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
if (realtimeInterval) {
|
||||
clearInterval(realtimeInterval);
|
||||
console.log('Realtime interval cleared');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -358,7 +338,6 @@ const changeYear = (year) => {
|
||||
activeYear.value = year;
|
||||
};
|
||||
|
||||
// --- DATA UTILITY FUNCTIONS ---
|
||||
const getKpiData = () => {
|
||||
return [
|
||||
{ name: 'Antrean Online', value: 150 },
|
||||
@@ -385,9 +364,7 @@ const convertJsonToCsv = (jsonData, fields) => {
|
||||
|
||||
return header + '\n' + csv;
|
||||
};
|
||||
// --- END DATA UTILITY FUNCTIONS ---
|
||||
|
||||
// --- EXPORT FUNCTION HANDLER ---
|
||||
const downloadFile = (data, filename, mimeType) => {
|
||||
const dataStr = `data:${mimeType};charset=utf-8,` + encodeURIComponent(data);
|
||||
const downloadAnchorNode = document.createElement('a');
|
||||
@@ -425,9 +402,7 @@ const handleExport = (type) => {
|
||||
alert("PDF (.pdf) export requires client-side libraries (like jsPDF) or a server service. Simulating download... 📝");
|
||||
}
|
||||
};
|
||||
// --- END EXPORT FUNCTION HANDLER ---
|
||||
|
||||
// --- CHART DATA LOGIC ---
|
||||
const processQueueData = (data, period) => {
|
||||
const grouped = {};
|
||||
const sortedDates = data.map(item => ({
|
||||
@@ -473,12 +448,12 @@ const processQueueData = (data, period) => {
|
||||
{
|
||||
label: `Attendees`,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#5FB7FF',
|
||||
borderColor: '#0671E0',
|
||||
data: finalAttendees,
|
||||
yAxisID: 'y1',
|
||||
type: 'line',
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
pointBackgroundColor: '#0663C7',
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
},
|
||||
@@ -530,13 +505,13 @@ const barData = computed(() => {
|
||||
},
|
||||
{
|
||||
label: `Conv. Rate (%)`,
|
||||
borderColor: '#5FB7FF',
|
||||
borderColor: '#0671E0',
|
||||
backgroundColor: 'transparent',
|
||||
data: conversionData,
|
||||
yAxisID: 'y2',
|
||||
type: 'line',
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: '#4A5F7A',
|
||||
pointBackgroundColor: '#0663C7',
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
}
|
||||
@@ -574,8 +549,8 @@ const pieData = ref({
|
||||
labels: ['Offline Registrants', 'Online Registrants'],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: ['#5FB7FF', '#FFB95F'],
|
||||
hoverBackgroundColor: ['#1B98FF ', '#FF9B1B'],
|
||||
backgroundColor: ['#0671E0', '#FFB95F'],
|
||||
hoverBackgroundColor: ['#0053AD', '#FF9B1B'],
|
||||
data: [759, 1876],
|
||||
borderWidth: 3,
|
||||
borderColor: '#ffffff',
|
||||
@@ -602,90 +577,183 @@ const pieOptions = ref({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Color Palette */
|
||||
<style scoped lang="scss">
|
||||
/* Background */
|
||||
.dashboard-bg {
|
||||
background-color: #FAFAFA;
|
||||
background: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.card-style {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
/* Typography */
|
||||
.dashboard-title {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--color-neutral-900);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Text Colors */
|
||||
.primary-text {
|
||||
color: #2C3E50 !important;
|
||||
.dashboard-subtitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-neutral-700);
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: #7B8794 !important;
|
||||
.user-name {
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.secondary-light {
|
||||
color: #7B8794 !important;
|
||||
}
|
||||
|
||||
.primary-accent {
|
||||
color: #FF9B1B !important;
|
||||
}
|
||||
|
||||
.secondary-accent {
|
||||
color: #1B98FF !important;
|
||||
}
|
||||
|
||||
/* Buttons & Interactive Elements */
|
||||
/* Export Button */
|
||||
.export-btn {
|
||||
transition: all 0.3s ease;
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(255, 155, 27, 0.2);
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: #E0916E !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Card Hover Effect - Subtle and Clean */
|
||||
.hover-effect {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Chip Transition */
|
||||
.transition-chip {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.transition-chip:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Date Chip Styling */
|
||||
/* Date Chip */
|
||||
.date-chip {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
}
|
||||
|
||||
/* Chart Card Fixed Height */
|
||||
/* Stat Cards */
|
||||
.stat-card {
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.stat-card-primary {
|
||||
border-left-color: var(--color-primary-600);
|
||||
}
|
||||
|
||||
.stat-card-secondary {
|
||||
border-left-color: var(--color-secondary-600);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
color: var(--color-neutral-900);
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-700);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Chart Cards */
|
||||
.chart-card {
|
||||
background: var(--color-neutral-100);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
min-height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.period-toggle {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.year-chip {
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.year-chip:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.live-chip {
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.chart-card .v-card-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-text {
|
||||
margin-top: 16px;
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 960px) {
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.d-flex.justify-space-between {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.d-flex.align-center .mr-4 {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,817 @@
|
||||
<!-- pages/MasterPenunjang.vue -->
|
||||
<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-clipboard-pulse</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h2 class="page-title">Master Penunjang</h2>
|
||||
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Layanan Penunjang</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
color="white"
|
||||
elevation="0"
|
||||
class="add-btn"
|
||||
@click="openTambahDialog"
|
||||
>
|
||||
<v-icon left size="20">mdi-plus-circle</v-icon>
|
||||
Tambah Penunjang
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="masterStore.penunjangData"
|
||||
:items-per-page="10"
|
||||
class="elevation-0 data-table"
|
||||
>
|
||||
<template #item.jenis="{ item }">
|
||||
<v-chip
|
||||
size="small"
|
||||
:class="item.jenis === 'Medis' ? 'chip-medis' : 'chip-non-medis'"
|
||||
>
|
||||
{{ item.jenis }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.shift="{ item }">
|
||||
<v-chip size="small" class="chip-secondary">
|
||||
{{ item.shift }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.totalQuota="{ item }">
|
||||
<span class="body-2 text-semibold">{{ item.totalQuota }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<v-chip
|
||||
size="small"
|
||||
:class="item.status === 'Aktif' ? 'chip-active' : 'chip-inactive'"
|
||||
>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.aksi="{ item }">
|
||||
<v-btn
|
||||
size="small"
|
||||
class="btn-edit mr-2"
|
||||
variant="flat"
|
||||
@click="openEditDialog(item)"
|
||||
>
|
||||
<v-icon size="16" left>mdi-pencil</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
class="btn-delete"
|
||||
variant="outlined"
|
||||
@click="handleDelete(item)"
|
||||
>
|
||||
<v-icon size="16" left>mdi-delete</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Dialog Tambah/Edit - COMPACT VERSION -->
|
||||
<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 ? 'Edit Penunjang' : 'Tambah Penunjang' }}</span>
|
||||
<v-btn icon variant="text" size="small" class="btn-close" @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text class="dialog-content-compact">
|
||||
<v-form ref="formRef">
|
||||
<!-- Informasi Dasar -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<v-icon size="18" class="mr-2">mdi-information-outline</v-icon>
|
||||
<span class="text-semibold">Informasi Dasar</span>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.kode"
|
||||
label="Kode Penunjang"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[v => !!v || 'Kode harus diisi']"
|
||||
hide-details="auto"
|
||||
placeholder="LAB"
|
||||
class="form-field"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.nama"
|
||||
label="Nama Penunjang"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[v => !!v || 'Nama harus diisi']"
|
||||
hide-details="auto"
|
||||
placeholder="Laboratorium"
|
||||
class="form-field"
|
||||
/>
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-select
|
||||
v-model="formData.jenis"
|
||||
label="Jenis Penunjang"
|
||||
:items="jenisPenunjangList"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[v => !!v || 'Jenis harus dipilih']"
|
||||
hide-details="auto"
|
||||
class="form-field"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-select
|
||||
v-model="formData.status"
|
||||
label="Status"
|
||||
:items="statusList"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
class="form-field"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<v-divider class="section-divider"/>
|
||||
|
||||
<!-- Konfigurasi Shift -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<v-icon size="18" class="mr-2">mdi-clock-outline</v-icon>
|
||||
<span class="text-semibold">Konfigurasi Shift & Kuota</span>
|
||||
</div>
|
||||
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
v-model.number="formData.shift"
|
||||
label="Jumlah Shift"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[v => !!v || 'Shift harus diisi', v => v > 0 || 'Minimal 1']"
|
||||
hide-details="auto"
|
||||
class="form-field"
|
||||
@update:model-value="updateShiftCount"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!--
|
||||
<div class="total-display">
|
||||
<span class="caption-text">Total Kuota</span>
|
||||
<v-chip size="small" color="success" variant="flat">
|
||||
<v-icon size="14" start>mdi-sigma</v-icon>
|
||||
{{ calculateTotalQuota() }}
|
||||
</v-chip>
|
||||
</div> -->
|
||||
|
||||
|
||||
<v-checkbox
|
||||
v-model="formData.autoShift"
|
||||
label="Auto Shift"
|
||||
hide-details
|
||||
color="primary"
|
||||
density="compact"
|
||||
class="form-field-checkbox"
|
||||
/>
|
||||
|
||||
<!-- Shift List -->
|
||||
<div class="shift-list">
|
||||
<div v-for="(shift, index) in formData.jamShiftList" :key="index" class="shift-row">
|
||||
<div class="shift-label">Shift {{ index + 1 }}</div>
|
||||
<v-row dense>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="shift.dari"
|
||||
label="Mulai"
|
||||
type="time"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="form-field-inline"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="shift.sampai"
|
||||
label="Selesai"
|
||||
type="time"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="form-field-inline"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="shift.kuota"
|
||||
label="Kuota"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
placeholder="0"
|
||||
class="form-field-inline"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1" class="d-flex align-center">
|
||||
<v-btn
|
||||
v-if="formData.jamShiftList.length > 1"
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="removeShift(index)"
|
||||
>
|
||||
<v-icon size="18">mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="section-divider"/>
|
||||
|
||||
<!-- Jadwal Operasional -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<v-icon size="18" class="mr-2">mdi-calendar-check</v-icon>
|
||||
<span class="text-semibold">Jadwal Operasional</span>
|
||||
</div>
|
||||
|
||||
<v-select
|
||||
v-model="formData.jadwalOperasional"
|
||||
:items="hariList.map(h => h.hari)"
|
||||
label="Pilih Hari Operasional"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
class="form-field"
|
||||
>
|
||||
<template #chip="{ item, props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
size="small"
|
||||
closable
|
||||
>
|
||||
{{ item.title }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
|
||||
<v-divider class="section-divider"/>
|
||||
|
||||
<!-- Keterangan -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<v-icon size="18" class="mr-2">mdi-note-text-outline</v-icon>
|
||||
<span class="text-semibold">Keterangan</span>
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-model="formData.keterangan"
|
||||
label="Keterangan Layanan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
rows="3"
|
||||
placeholder="Informasi tambahan tentang layanan penunjang..."
|
||||
class="form-field"
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-actions class="dialog-actions">
|
||||
<v-spacer/>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
class="btn-cancel"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<v-icon left size="18">mdi-close</v-icon>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
class="btn-submit"
|
||||
@click="submitForm"
|
||||
>
|
||||
<v-icon left size="18">mdi-content-save</v-icon>
|
||||
Simpan
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
<span class="body-3">{{ snackbar.message }}</span>
|
||||
<template #actions>
|
||||
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
|
||||
const masterStore = useMasterStore();
|
||||
const dialog = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
});
|
||||
|
||||
const headers = ref([
|
||||
{ title: 'No', value: 'no', sortable: true },
|
||||
{ title: 'Kode', value: 'kode', sortable: true },
|
||||
{ title: 'Nama Penunjang', value: 'nama', sortable: true },
|
||||
{ title: 'Jenis', value: 'jenis', sortable: true },
|
||||
{ title: 'Shift', value: 'shift', sortable: true },
|
||||
{ title: 'Total Kuota', value: 'totalQuota', sortable: true },
|
||||
{ title: 'Status', value: 'status', sortable: true },
|
||||
{ title: 'Aksi', value: 'aksi', sortable: false },
|
||||
]);
|
||||
|
||||
const jenisPenunjangList = ref([
|
||||
'Medis',
|
||||
'Non-Medis'
|
||||
]);
|
||||
|
||||
const statusList = ref([
|
||||
'Aktif',
|
||||
'Tidak Aktif'
|
||||
]);
|
||||
|
||||
const hariList = ref([
|
||||
{ no: 1, hari: 'Senin' },
|
||||
{ no: 2, hari: 'Selasa' },
|
||||
{ no: 3, hari: 'Rabu' },
|
||||
{ no: 4, hari: 'Kamis' },
|
||||
{ no: 5, hari: 'Jum\'at' },
|
||||
{ no: 6, hari: 'Sabtu' },
|
||||
{ no: 7, hari: 'Minggu' },
|
||||
]);
|
||||
|
||||
const formData = ref({
|
||||
id: null,
|
||||
kode: '',
|
||||
nama: '',
|
||||
jenis: 'Medis',
|
||||
shift: 1,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '15:00', kuota: 0 }],
|
||||
autoShift: false,
|
||||
jadwalOperasional: [],
|
||||
status: 'Aktif',
|
||||
keterangan: ''
|
||||
});
|
||||
|
||||
const calculateTotalQuota = () => {
|
||||
return formData.value.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const updateShiftCount = (newShiftCount) => {
|
||||
const currentCount = formData.value.jamShiftList.length;
|
||||
|
||||
if (newShiftCount > currentCount) {
|
||||
for (let i = currentCount; i < newShiftCount; i++) {
|
||||
let defaultDari = '07:00', defaultSampai = '15:00';
|
||||
|
||||
if (i === 1) { defaultDari = '13:00'; defaultSampai = '21:00'; }
|
||||
else if (i === 2) { defaultDari = '21:00'; defaultSampai = '07:00'; }
|
||||
else if (i > 2) {
|
||||
const prevShift = formData.value.jamShiftList[i - 1];
|
||||
const [prevHour] = prevShift.sampai.split(':');
|
||||
const nextHour = (parseInt(prevHour) + 1) % 24;
|
||||
defaultDari = `${String(nextHour).padStart(2, '0')}:00`;
|
||||
defaultSampai = `${String((nextHour + 8) % 24).padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
formData.value.jamShiftList.push({ dari: defaultDari, sampai: defaultSampai, kuota: 0 });
|
||||
}
|
||||
} else if (newShiftCount < currentCount && newShiftCount > 0) {
|
||||
formData.value.jamShiftList = formData.value.jamShiftList.slice(0, newShiftCount);
|
||||
}
|
||||
};
|
||||
|
||||
const removeShift = (index) => {
|
||||
if (formData.value.jamShiftList.length > 1) {
|
||||
formData.value.jamShiftList.splice(index, 1);
|
||||
formData.value.shift = formData.value.jamShiftList.length;
|
||||
}
|
||||
};
|
||||
|
||||
const openTambahDialog = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const openEditDialog = (item) => {
|
||||
isEdit.value = true;
|
||||
formData.value = {
|
||||
id: item.id,
|
||||
kode: item.kode,
|
||||
nama: item.nama,
|
||||
jenis: item.jenis,
|
||||
shift: item.shift,
|
||||
jamShiftList: JSON.parse(JSON.stringify(item.jamShiftList)),
|
||||
autoShift: item.autoShift || false,
|
||||
jadwalOperasional: [...(item.jadwalOperasional || [])],
|
||||
status: item.status,
|
||||
keterangan: item.keterangan || ''
|
||||
};
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialog.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: null,
|
||||
kode: '',
|
||||
nama: '',
|
||||
jenis: 'Medis',
|
||||
shift: 1,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '15:00', kuota: 0 }],
|
||||
autoShift: false,
|
||||
jadwalOperasional: [],
|
||||
status: 'Aktif',
|
||||
keterangan: ''
|
||||
};
|
||||
if (formRef.value) {
|
||||
formRef.value.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
const { valid } = await formRef.value.validate();
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
let result;
|
||||
if (isEdit.value) {
|
||||
result = masterStore.updatePenunjang(formData.value);
|
||||
} else {
|
||||
result = masterStore.addPenunjang(formData.value);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
snackbar.value = { show: true, message: result.message, color: 'success' };
|
||||
closeDialog();
|
||||
} else {
|
||||
snackbar.value = { show: true, message: result.message, color: 'error' };
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
if (confirm(`Hapus penunjang ${item.nama}?`)) {
|
||||
const result = masterStore.deletePenunjang(item.id);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: result.message,
|
||||
color: result.success ? 'success' : 'error'
|
||||
};
|
||||
}
|
||||
};
|
||||
</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;
|
||||
|
||||
$primary-700: #7c3aed;
|
||||
$primary-600: #8b5cf6;
|
||||
|
||||
$success-600: #009262;
|
||||
$success-300: #84DFC1;
|
||||
|
||||
$danger-600: #E02B1D;
|
||||
|
||||
$medis-600: #10b981;
|
||||
$medis-300: #d1fae5;
|
||||
|
||||
$non-medis-600: #f59e0b;
|
||||
$non-medis-300: #fef3c7;
|
||||
|
||||
// Font
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
* {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PAGE HEADER
|
||||
// ============================================
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 4px 16px rgba(139, 92, 246, 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: $primary-600 !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATA TABLE
|
||||
// ============================================
|
||||
.data-table {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.chip-medis {
|
||||
background-color: $medis-300 !important;
|
||||
color: $medis-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.chip-non-medis {
|
||||
background-color: $non-medis-300 !important;
|
||||
color: $non-medis-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.chip-secondary {
|
||||
background-color: $primary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.chip-active {
|
||||
background-color: $success-300 !important;
|
||||
color: $success-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.chip-inactive {
|
||||
background-color: $neutral-400 !important;
|
||||
color: $neutral-600 !important;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: $primary-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;
|
||||
}
|
||||
|
||||
.body-2 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.text-semibold {
|
||||
font-weight: $font-weight-semibold !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DIALOG - COMPACT VERSION
|
||||
// ============================================
|
||||
.dialog-card {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
background: linear-gradient(135deg, $primary-600 0%, $primary-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-compact {
|
||||
padding: 20px 24px !important;
|
||||
background: $neutral-100;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 16px 24px;
|
||||
background: $neutral-100;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FORM SECTIONS - COMPACT
|
||||
// ============================================
|
||||
.form-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: $primary-600;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
margin: 16px 0;
|
||||
border-color: $neutral-400 !important;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-field-inline {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-field-checkbox {
|
||||
margin-bottom: 12px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
// Total Display
|
||||
.total-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption-text {
|
||||
font-size: 11px;
|
||||
color: $neutral-700;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
// Shift List
|
||||
.shift-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.shift-row {
|
||||
background: $neutral-300;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid $neutral-400;
|
||||
}
|
||||
|
||||
.shift-label {
|
||||
font-size: 11px;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $primary-600;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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: $primary-600 !important;
|
||||
color: $neutral-100 !important;
|
||||
text-transform: none;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.body-3 {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -244,7 +244,7 @@ const headers = ref([
|
||||
{ title: "Nama Screen", value: "namaScreen", sortable: true },
|
||||
{ title: "Nomor Screen", value: "nomorScreen", sortable: true },
|
||||
{ title: "Klinik Ditampilkan", value: "klinik", sortable: false },
|
||||
{ title: "Actions", value: "actions", sortable: false, width: "250px" },
|
||||
{ title: "Actions", value: "actions", sortable: false, width: "auto" },
|
||||
]);
|
||||
|
||||
const screenItems = ref([
|
||||
|
||||
@@ -1,256 +1,87 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-card
|
||||
class="elevation-3 rounded-xl header-banner mx-6 mt-6 mb-n6"
|
||||
style="position: relative; z-index: 10;"
|
||||
<v-container>
|
||||
<v-app>
|
||||
<!-- Page Header -->
|
||||
<PageHeader
|
||||
icon="mdi-shield-check"
|
||||
title="Verifikasi Akun"
|
||||
subtitle="Manajemen Pasien Digital"
|
||||
:show-add-button="false"
|
||||
theme="primary"
|
||||
>
|
||||
<v-container fluid class="py-5 px-6 d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<div class="icon-container">
|
||||
<v-icon size="44" class="primary-icon mr-4">mdi-shield-check</v-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-black primary-text mb-1">Verifikasi Akun</h1>
|
||||
<p class="text-subtitle-1 secondary-text mb-0">Manajemen Pasien Digital</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-chip color="#5FB7FF" class="text-white font-weight-black px-5" variant="flat" rounded="xl" size="large">
|
||||
<template #actions>
|
||||
<v-chip color="secondary-600" class="admin-chip" variant="flat" rounded="xl" size="large">
|
||||
<v-icon start color="white">mdi-account-star</v-icon>
|
||||
Admin
|
||||
<span class="chip-text">Admin</span>
|
||||
</v-chip>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<v-main class="bg-main">
|
||||
<v-main class="main-bg">
|
||||
<v-container fluid class="pa-6 pt-8 pb-12">
|
||||
<v-card class="content-card">
|
||||
<!-- Search & Tabs -->
|
||||
<VerificationSearchTabs
|
||||
v-model:search-query="search"
|
||||
v-model:selected-tab="filterStatus"
|
||||
:all-count="patients.length"
|
||||
:pending-count="unverifiedPatients.length"
|
||||
:verified-count="verifiedPatients.length"
|
||||
/>
|
||||
|
||||
<v-card class="pa-0 mb-6 elevation-3 rounded-xl overflow-hidden card-style">
|
||||
|
||||
<v-row class="ma-0 pa-6 align-center search-section">
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
density="comfortable"
|
||||
label="Cari pasien berdasarkan nama atau RM..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
single-line
|
||||
color="#D17A4A"
|
||||
bg-color="white"
|
||||
class="search-field text-body-1"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-tabs
|
||||
v-model="filterStatus"
|
||||
color="#D17A4A"
|
||||
align-tabs="end"
|
||||
class="pt-0 filter-tabs"
|
||||
slider-color="#D17A4A"
|
||||
>
|
||||
<v-tab :value="0" class="font-weight-bold text-button">
|
||||
SEMUA <v-chip size="small" color="#7B8794" class="ml-2 text-white">{{ patients.length }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab :value="1" class="font-weight-bold text-button">
|
||||
PENDING <v-chip size="small" color="#D17A4A" class="ml-2 text-white">{{ unverifiedPatients.length }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab :value="2" class="font-weight-bold text-button">
|
||||
VERIFIED <v-chip size="small" color="#4A5F7A" class="ml-2 text-white">{{ verifiedPatients.length }}</v-chip>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-text class="pa-0 bg-white">
|
||||
<v-list lines="two" class="pa-0">
|
||||
<v-list-item
|
||||
v-for="(patient, index) in filteredPatients"
|
||||
:key="patient.rm"
|
||||
class="patient-item py-5 px-6"
|
||||
:class="{ 'verified-item': patient.status === 'Terverifikasi' }"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="patient.status === 'Terverifikasi' ? '#32A3FF' : '#FFA532'" size="64" class="patient-avatar">
|
||||
<v-icon size="36" color="white">
|
||||
{{ patient.status === 'Terverifikasi' ? 'mdi-check-decagram' : 'mdi-clock-alert' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-h5 font-weight-black mb-2 primary-text">
|
||||
{{ patient.nama }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="mt-2">
|
||||
<v-row dense class="text-body-1">
|
||||
<v-col cols="12" sm="3" md="2" class="py-1">
|
||||
<v-chip size="default" color="#E3F2FD" class="font-weight-bold chip-rm" variant="flat">
|
||||
<v-icon start size="18" color="#4A5F7A">mdi-file-document</v-icon>
|
||||
<span style="color: #4A5F7A;">{{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" md="6" class="py-1 secondary-text d-flex align-center">
|
||||
<v-icon size="18" class="mr-2" color="#7B8794">mdi-map-marker</v-icon>
|
||||
<span class="text-body-1">{{ patient.alamat }}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4" md="4" class="py-1 secondary-text d-flex align-center">
|
||||
<v-icon size="18" class="mr-2" color="#7B8794">mdi-phone</v-icon>
|
||||
<span class="text-body-1">{{ patient.telepon || 'Belum diisi' }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
color="#FFA532"
|
||||
size="x-large"
|
||||
@click="openDaftarModal(patient)"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
variant="flat"
|
||||
class="font-weight-black elevation-2 action-btn px-8 text-h6 text-white"
|
||||
rounded="xl"
|
||||
>
|
||||
VERIFIKASI
|
||||
</v-btn>
|
||||
<v-chip
|
||||
v-else
|
||||
color="#32A3FF"
|
||||
size="x-large"
|
||||
variant="flat"
|
||||
class="font-weight-black px-6 text-h6 text-white"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon start size="24">mdi-shield-check</v-icon>
|
||||
VERIFIED
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="filteredPatients.length === 0">
|
||||
<v-list-item-title class="text-center py-8 text-h5 secondary-text">
|
||||
{{ noDataText }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<!-- Patient List -->
|
||||
<PatientVerificationList
|
||||
:patients="filteredPatients"
|
||||
:empty-message="noDataText"
|
||||
@verify="openDaftarModal"
|
||||
/>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="isModalOpen" max-width="600" transition="dialog-bottom-transition" scrollable>
|
||||
<v-card class="pa-0 rounded-xl overflow-hidden card-style">
|
||||
<div class="modal-header pa-7">
|
||||
<v-icon size="72" color="white" class="mb-3">mdi-qrcode-scan</v-icon>
|
||||
<h2 class="text-h3 font-weight-black text-white mb-2">Aktivasi Akun</h2>
|
||||
<p class="text-h6 text-white opacity-90">{{ selectedPatient.nama }}</p>
|
||||
<v-chip color="white" class="font-weight-bold mt-2" variant="flat" size="default">
|
||||
<span style="color: #D17A4A;">RM: {{ selectedPatient.rm }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-card-text class="pa-7">
|
||||
<v-text-field
|
||||
v-model="tempTelepon"
|
||||
:disabled="qrCodeGenerated"
|
||||
label="Nomor Telepon Pasien"
|
||||
placeholder="08xxxxxxxxxx"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:color="qrCodeGenerated ? 'grey' : '#D17A4A'"
|
||||
:rules="[v => v.length >= 8 || 'Min. 8 digit']"
|
||||
prepend-inner-icon="mdi-phone"
|
||||
class="mb-3 text-body-1"
|
||||
base-color="grey-darken-1"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
v-if="!qrCodeGenerated"
|
||||
:disabled="!isTeleponValid"
|
||||
color="#5FB7FF"
|
||||
block
|
||||
size="x-large"
|
||||
class="mt-3 font-weight-black elevation-2 text-h5 text-white"
|
||||
@click="generateQrCode"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon left size="32">mdi-qrcode-plus</v-icon>
|
||||
Generate QR Code
|
||||
</v-btn>
|
||||
|
||||
<div v-else class="text-center mt-6 pa-4 pa-sm-7 qr-container rounded-xl">
|
||||
<div class="pulse-icon mb-3 mb-sm-4">
|
||||
<v-icon color="#4A5F7A" :size="display.xs.value ? 48 : 64">mdi-cellphone-check</v-icon>
|
||||
</div>
|
||||
<h3 class="text-h5 text-sm-h4 font-weight-black mb-2 mb-sm-3" style="color: #4A5F7A;">Pindai QR Code</h3>
|
||||
<p class="text-body-2 text-sm-body-1 secondary-text mb-4 mb-sm-6">Arahkan kamera smartphone ke QR code</p>
|
||||
|
||||
<div class="d-flex justify-center mb-3 mb-sm-4">
|
||||
<div class="qr-frame">
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="qrSize"
|
||||
level="H"
|
||||
:foreground="'#4A5F7A'"
|
||||
:background="'#FFFFFF'"
|
||||
class="qr-code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="qrCodeGenerated" class="pa-7 pt-0">
|
||||
<v-btn
|
||||
color="#4A5F7A"
|
||||
variant="outlined"
|
||||
@click="reloadQr"
|
||||
prepend-icon="mdi-reload"
|
||||
size="x-large"
|
||||
class="font-weight-bold flex-grow-1 text-body-1"
|
||||
rounded="xl"
|
||||
>
|
||||
Reload QR
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="#D17A4A"
|
||||
variant="flat"
|
||||
@click="completeVerification"
|
||||
prepend-icon="mdi-check-circle"
|
||||
size="x-large"
|
||||
class="font-weight-black flex-grow-1 elevation-2 text-body-1"
|
||||
rounded="xl"
|
||||
>
|
||||
Selesai
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-card-actions v-else class="pa-7 pt-0">
|
||||
<v-btn color="#7B8794" variant="text" @click="closeModal" size="x-large" block rounded="xl" class="font-weight-bold text-body-1">
|
||||
Batal
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!-- QR Verification Dialog -->
|
||||
<QrVerificationDialog
|
||||
v-model="isModalOpen"
|
||||
:patient="selectedPatient"
|
||||
v-model:phone-number="tempTelepon"
|
||||
:qr-generated="qrCodeGenerated"
|
||||
:is-mobile="display.xs.value"
|
||||
@generate="generateQrCode"
|
||||
@reload="reloadQr"
|
||||
@complete="completeVerification"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #qr-code>
|
||||
<qrcode-vue
|
||||
:value="qrCodeData"
|
||||
:size="qrSize"
|
||||
level="H"
|
||||
:foreground="'#0663C7'"
|
||||
:background="'#FFFFFF'"
|
||||
class="qr-code"
|
||||
/>
|
||||
</template>
|
||||
</QrVerificationDialog>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import VerificationSearchTabs from '@/components/verification/VerificationSearchTabs.vue';
|
||||
import PatientVerificationList from '@/components/verification/PatientVerificationList.vue';
|
||||
import QrVerificationDialog from '@/components/verification/QrVerificationDialog.vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth']
|
||||
})
|
||||
middleware: ['auth']
|
||||
});
|
||||
|
||||
const display = useDisplay();
|
||||
|
||||
// --- MOCK DATA SIMRS ---
|
||||
// Data
|
||||
const patients = ref([
|
||||
{ nama: 'Budi Santoso', rm: '00123456', alamat: 'Jl. Melati No. 10', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ nama: 'Siti Aminah', rm: '00123457', alamat: 'Perum. Indah Blok A', telepon: '081234567890', status: 'Terverifikasi' },
|
||||
@@ -259,14 +90,24 @@ const patients = ref([
|
||||
{ nama: 'Rina Wijaya', rm: '00123460', alamat: 'Jl. Sudirman 21', telepon: '', status: 'Belum Terverifikasi' },
|
||||
]);
|
||||
|
||||
// --- STATE DAN FILTERING ---
|
||||
const filterStatus = ref(0);
|
||||
const filterStatus = ref(0);
|
||||
const search = ref('');
|
||||
const noDataText = computed(() => search.value ? 'Tidak ada data pasien yang cocok dengan pencarian.' : 'Tidak ada pasien dengan status ini.');
|
||||
const isModalOpen = ref(false);
|
||||
const selectedPatient = ref({});
|
||||
const tempTelepon = ref('');
|
||||
const qrCodeGenerated = ref(false);
|
||||
|
||||
// Computed
|
||||
const noDataText = computed(() =>
|
||||
search.value
|
||||
? 'Tidak ada data pasien yang cocok dengan pencarian.'
|
||||
: 'Tidak ada pasien dengan status ini.'
|
||||
);
|
||||
|
||||
const unverifiedPatients = computed(() =>
|
||||
patients.value.filter((p) => p.status === 'Belum Terverifikasi')
|
||||
);
|
||||
|
||||
const verifiedPatients = computed(() =>
|
||||
patients.value.filter((p) => p.status === 'Terverifikasi')
|
||||
);
|
||||
@@ -289,33 +130,24 @@ const filteredPatients = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// --- STATE MODAL QR CODE ---
|
||||
const isModalOpen = ref(false);
|
||||
const selectedPatient = ref({});
|
||||
const tempTelepon = ref('');
|
||||
const qrCodeGenerated = ref(false);
|
||||
|
||||
const isTeleponValid = computed(() => tempTelepon.value.length >= 8);
|
||||
|
||||
const qrCodeData = computed(() => {
|
||||
return qrCodeGenerated.value
|
||||
? JSON.stringify({
|
||||
rm: selectedPatient.value.rm,
|
||||
phone: tempTelepon.value,
|
||||
action: 'verify-account',
|
||||
timestamp: Date.now()
|
||||
})
|
||||
: '';
|
||||
return qrCodeGenerated.value
|
||||
? JSON.stringify({
|
||||
rm: selectedPatient.value.rm,
|
||||
phone: tempTelepon.value,
|
||||
action: 'verify-account',
|
||||
timestamp: Date.now()
|
||||
})
|
||||
: '';
|
||||
});
|
||||
|
||||
// Adaptive QR code size based on screen size
|
||||
const qrSize = computed(() => {
|
||||
if (display.xs.value) return 200; // Extra small screens (mobile)
|
||||
if (display.sm.value) return 240; // Small screens (large phones)
|
||||
return 280; // Medium and larger screens
|
||||
if (display.xs.value) return 200;
|
||||
if (display.sm.value) return 240;
|
||||
return 280;
|
||||
});
|
||||
|
||||
// --- LOGIC MODAL ---
|
||||
|
||||
// Methods
|
||||
const openDaftarModal = (patient) => {
|
||||
selectedPatient.value = patient;
|
||||
tempTelepon.value = patient.telepon || '';
|
||||
@@ -331,7 +163,7 @@ const closeModal = () => {
|
||||
};
|
||||
|
||||
const generateQrCode = () => {
|
||||
if (isTeleponValid.value) {
|
||||
if (tempTelepon.value.length >= 8) {
|
||||
console.log(`[API CALL] Generating QR for ${selectedPatient.value.nama}. QR Data: ${qrCodeData.value}`);
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
@@ -341,7 +173,7 @@ const generateQrCode = () => {
|
||||
|
||||
const reloadQr = () => {
|
||||
console.log('[API CALL] Reloading QR Code...');
|
||||
qrCodeGenerated.value = false;
|
||||
qrCodeGenerated.value = false;
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
console.log('QR Code reloaded.');
|
||||
@@ -358,215 +190,48 @@ const completeVerification = () => {
|
||||
patients.value[patientIndex].telepon = tempTelepon.value;
|
||||
}
|
||||
|
||||
filterStatus.value = 2;
|
||||
filterStatus.value = 2;
|
||||
closeModal();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Background */
|
||||
.bg-main {
|
||||
background-color: #FAFAFA;
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-300: #F5F7FA;
|
||||
$neutral-500: #ABBED1;
|
||||
$secondary-600: #0671E0;
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
.main-bg {
|
||||
background: $neutral-300;
|
||||
min-height: 100vh;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.card-style {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
.admin-chip {
|
||||
font-weight: $font-weight-bold;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
/* Text Colors */
|
||||
.primary-text {
|
||||
color: #2C3E50 !important;
|
||||
.chip-text {
|
||||
color: $neutral-100;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: #7B8794 !important;
|
||||
}
|
||||
|
||||
.primary-icon {
|
||||
color: #FF9B1B !important;
|
||||
}
|
||||
|
||||
/* Header Banner */
|
||||
.header-banner {
|
||||
background-color: #E8F5FF;
|
||||
.content-card {
|
||||
background: $neutral-100;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.header-banner:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
background-color: rgba(209, 122, 74, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-section {
|
||||
background-color: #FAFAFA;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-field {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Patient List Items */
|
||||
.patient-item {
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.patient-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background: #D17A4A;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.patient-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.patient-item:hover {
|
||||
background: linear-gradient(90deg, #FFF5EE 0%, #ffffff 100%) !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.verified-item {
|
||||
background: linear-gradient(90deg, #F0F4F8 0%, #ffffff 100%) !important;
|
||||
}
|
||||
|
||||
.verified-item::before {
|
||||
background: #4A5F7A !important;
|
||||
}
|
||||
|
||||
.verified-item:hover {
|
||||
background: linear-gradient(90deg, #E8EFF5 0%, #ffffff 100%) !important;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid white;
|
||||
}
|
||||
|
||||
.chip-rm {
|
||||
border: 1px solid rgba(74, 95, 122, 0.2);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
text-transform: none !important;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(209, 122, 74, 0.3) !important;
|
||||
background-color: #E0916E !important;
|
||||
}
|
||||
|
||||
/* Modal Styling */
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #4A5F7A 0%, #5D7290 100%);
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(74, 95, 122, 0.2);
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background: linear-gradient(135deg, #F0F4F8 0%, #E8EFF5 100%);
|
||||
border: 3px solid #4A5F7A;
|
||||
box-shadow: 0 6px 20px rgba(74, 95, 122, 0.15);
|
||||
}
|
||||
|
||||
.qr-frame {
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
display: inline-block;
|
||||
border: 3px solid #4A5F7A;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.qr-frame {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
border: 4px solid #4A5F7A;
|
||||
}
|
||||
border: 1px solid $neutral-500;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pulse-icon {
|
||||
animation: pulse-scale 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-scale {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 960px) {
|
||||
.patient-item {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.header-banner h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.header-banner p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 1rem;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.text-h5 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-banner .v-container {
|
||||
padding-left: 1.5rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user