update card, update design system, update layout anjungan
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
@use 'sass:map';
|
||||
|
||||
// Color Palette Variables
|
||||
$neutral-900: #212121;
|
||||
$neutral-800: #4D4D4D;
|
||||
$neutral-700: #717171;
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
</div>
|
||||
|
||||
<div class="action-grid">
|
||||
<v-btn color="success-600" variant="flat" block size="large" @click="$emit('action', 'check-in')">
|
||||
<v-btn class="text-white" color="success-600" variant="flat" block size="large" @click="$emit('action', '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="$emit('action', 'terlambat')">
|
||||
<v-btn class="text-white" color="primary-600" variant="flat" block size="large" @click="$emit('action', '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="$emit('action', 'pending')">
|
||||
<v-btn class="text-white" color="danger-600" variant="flat" block size="large" @click="$emit('action', 'pending')">
|
||||
<v-icon start size="20">mdi-pause</v-icon>
|
||||
Pending
|
||||
</v-btn>
|
||||
<v-btn color="secondary-600" variant="flat" block size="large" @click="$emit('change-klinik')">
|
||||
<v-btn class="text-white" color="secondary-600" variant="flat" block size="large" @click="$emit('change-klinik')">
|
||||
<v-icon start size="20">mdi-swap-horizontal</v-icon>
|
||||
{{ changeButtonText }}
|
||||
</v-btn>
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-card class="patient-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Header: Queue Number & Status -->
|
||||
<div class="card-header">
|
||||
<div class="queue-number">{{ patient.noAntrian.split(" |")[0] }}</div>
|
||||
<v-chip
|
||||
:color="getStatusColor(patient.status)"
|
||||
size="small"
|
||||
class="status-chip"
|
||||
>
|
||||
{{ getStatusLabel(patient.status) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Patient Info Grid -->
|
||||
<div class="patient-info mt-3">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Barcode:</span>
|
||||
<span class="info-value">{{ patient.barcode }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Jam Panggil:</span>
|
||||
<span class="info-value">{{ patient.jamPanggil }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Klinik:</span>
|
||||
<v-chip size="small" variant="outlined" class="klinik-chip">
|
||||
{{ patient.klinik }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Fast Track:</span>
|
||||
<span class="info-value">{{ patient.fastTrack }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Pembayaran:</span>
|
||||
<span class="info-value">{{ patient.pembayaran }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="card-actions mt-4">
|
||||
<v-btn
|
||||
v-if="patient.status === 'diloket'"
|
||||
block
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
@click="$emit('action', patient, 'proses')"
|
||||
>
|
||||
<v-icon start size="18">mdi-account-check</v-icon>
|
||||
Proses
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="patient.status === 'terlambat'"
|
||||
block
|
||||
color="success-600"
|
||||
variant="flat"
|
||||
class="text-white"
|
||||
@click="$emit('action', patient, 'aktifkan')"
|
||||
>
|
||||
<v-icon start size="18">mdi-check-circle</v-icon>
|
||||
Aktifkan
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="patient.status === 'pending'"
|
||||
block
|
||||
color="success-600"
|
||||
class="text-white"
|
||||
variant="flat"
|
||||
@click="$emit('action', patient, 'proses')"
|
||||
>
|
||||
<v-icon start size="18">mdi-play-circle</v-icon>
|
||||
Proses
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Card Menu (Optional) -->
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="card-menu-btn"
|
||||
v-bind="props"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="$emit('view-details', patient)">
|
||||
<v-list-item-title>Lihat Detail</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
patient: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['action', 'view-details']);
|
||||
|
||||
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 getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
diloket: "Di Loket",
|
||||
terlambat: "Terlambat",
|
||||
pending: "Pending"
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.patient-card {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
transition: all 0.2s ease;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-neutral-400);
|
||||
}
|
||||
|
||||
.queue-number {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--color-neutral-600);
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--color-neutral-900);
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.klinik-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-actions .v-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.card-menu-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.patient-card:hover .card-menu-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-card class="patient-table-card" elevation="0">
|
||||
<v-card class="patient-data-container" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Table Header with Filters -->
|
||||
<div class="table-header mb-4">
|
||||
<!-- Header with Filters -->
|
||||
<div class="data-header mb-4">
|
||||
<div class="section-label">DATA PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
@@ -28,73 +28,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filteredItems"
|
||||
:search="searchModel"
|
||||
class="patient-table"
|
||||
:items-per-page="itemsPerPage"
|
||||
density="comfortable"
|
||||
>
|
||||
<template #item.noAntrian="{ item }">
|
||||
<div class="queue-number">{{ item.noAntrian.split(" |")[0] }}</div>
|
||||
</template>
|
||||
<!-- Patient Cards Grid -->
|
||||
<div v-if="filteredAndSearchedItems.length > 0" class="patient-grid">
|
||||
<PatientCard
|
||||
v-for="(patient, index) in paginatedItems"
|
||||
:key="`${patient.barcode}-${index}`"
|
||||
:patient="patient"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
class="status-chip"
|
||||
>
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="64" color="neutral-500">mdi-account-search</v-icon>
|
||||
<div class="empty-text mt-3">Tidak ada data pasien</div>
|
||||
<div class="empty-subtext">Data akan muncul ketika ada pasien yang terdaftar</div>
|
||||
</div>
|
||||
|
||||
<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="$emit('action', item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'terlambat'"
|
||||
size="small"
|
||||
color="success-600"
|
||||
variant="flat"
|
||||
@click="$emit('action', item, 'aktifkan')"
|
||||
>
|
||||
Aktifkan
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="item.status === 'pending'"
|
||||
size="small"
|
||||
color="success-600"
|
||||
variant="flat"
|
||||
@click="$emit('action', item, 'proses')"
|
||||
>
|
||||
Proses
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<!-- Pagination -->
|
||||
<div v-if="filteredAndSearchedItems.length > itemsPerPage" class="pagination-container mt-4">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="totalPages"
|
||||
:total-visible="7"
|
||||
rounded="circle"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import PatientCard from './PatientCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
@@ -123,7 +89,7 @@ const props = defineProps({
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 10
|
||||
default: 9
|
||||
},
|
||||
statusLabels: {
|
||||
type: Object,
|
||||
@@ -138,14 +104,22 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:selectedStatus', 'update:searchQuery', 'action']);
|
||||
|
||||
const currentPage = ref(1);
|
||||
|
||||
const selectedStatusModel = computed({
|
||||
get: () => props.selectedStatus,
|
||||
set: (value) => emit('update:selectedStatus', value)
|
||||
set: (value) => {
|
||||
currentPage.value = 1; // Reset to first page on filter change
|
||||
emit('update:selectedStatus', value);
|
||||
}
|
||||
});
|
||||
|
||||
const searchModel = computed({
|
||||
get: () => props.searchQuery,
|
||||
set: (value) => emit('update:searchQuery', value)
|
||||
set: (value) => {
|
||||
currentPage.value = 1; // Reset to first page on search
|
||||
emit('update:searchQuery', value);
|
||||
}
|
||||
});
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
@@ -160,39 +134,34 @@ const filteredItems = computed(() => {
|
||||
return props.items.filter(p => p.status === selectedStatusModel.value);
|
||||
});
|
||||
|
||||
const headers = [
|
||||
{ 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 filteredAndSearchedItems = computed(() => {
|
||||
if (!searchModel.value) return filteredItems.value;
|
||||
|
||||
const searchLower = searchModel.value.toLowerCase();
|
||||
return filteredItems.value.filter(patient =>
|
||||
patient.barcode?.toLowerCase().includes(searchLower) ||
|
||||
patient.noAntrian?.toLowerCase().includes(searchLower) ||
|
||||
patient.klinik?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
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 totalPages = computed(() =>
|
||||
Math.ceil(filteredAndSearchedItems.value.length / props.itemsPerPage)
|
||||
);
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
diloket: "Di Loket",
|
||||
terlambat: "Terlambat",
|
||||
pending: "Pending"
|
||||
};
|
||||
return labels[status] || status;
|
||||
const paginatedItems = computed(() => {
|
||||
const start = (currentPage.value - 1) * props.itemsPerPage;
|
||||
const end = start + props.itemsPerPage;
|
||||
return filteredAndSearchedItems.value.slice(start, end);
|
||||
});
|
||||
|
||||
const handleAction = (patient, action) => {
|
||||
emit('action', patient, action);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.patient-table-card {
|
||||
.patient-data-container {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
@@ -206,7 +175,7 @@ const getStatusLabel = (status) => {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
.data-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -230,6 +199,11 @@ const getStatusLabel = (status) => {
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
color: var(--color-neutral-600);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-neutral-300);
|
||||
}
|
||||
}
|
||||
|
||||
.status-filter .v-chip.active-chip {
|
||||
@@ -239,56 +213,45 @@ const getStatusLabel = (status) => {
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 250px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
:deep(.patient-table) {
|
||||
border-radius: 8px;
|
||||
.patient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:deep(.patient-table .v-data-table__thead) {
|
||||
background: var(--color-neutral-300);
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
: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 {
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-700);
|
||||
}
|
||||
|
||||
.klinik-chip {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
.empty-subtext {
|
||||
font-size: 13px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.action-buttons-table .v-btn {
|
||||
text-transform: none;
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-neutral-400);
|
||||
}
|
||||
|
||||
:deep(.v-pagination__item) {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@@ -300,5 +263,22 @@ const getStatusLabel = (status) => {
|
||||
.search-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.patient-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 961px) and (max-width: 1264px) {
|
||||
.patient-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1265px) {
|
||||
.patient-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<v-card-text class="pa-0 bg-white">
|
||||
<v-list lines="two" class="pa-0">
|
||||
<v-list-item
|
||||
v-for="patient in patients"
|
||||
:key="patient.rm"
|
||||
class="patient-item"
|
||||
:class="{ 'verified-item': patient.status === 'Terverifikasi' }"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
:color="patient.status === 'Terverifikasi' ? 'secondary-600' : 'primary-600'"
|
||||
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="patient-name">
|
||||
{{ patient.nama }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="mt-2">
|
||||
<v-row dense class="patient-info">
|
||||
<v-col cols="12" sm="3" md="2" class="py-1">
|
||||
<v-chip size="default" color="primary-200" class="chip-rm" variant="flat">
|
||||
<v-icon start size="18" color="secondary-600">mdi-file-document</v-icon>
|
||||
<span class="chip-rm-text">{{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" md="6" class="py-1 info-item">
|
||||
<v-icon size="18" class="mr-2" color="neutral-600">mdi-map-marker</v-icon>
|
||||
<span>{{ patient.alamat }}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="4" md="4" class="py-1 info-item">
|
||||
<v-icon size="18" class="mr-2" color="neutral-600">mdi-phone</v-icon>
|
||||
<span>{{ patient.telepon || 'Belum diisi' }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append>
|
||||
<v-btn
|
||||
v-if="patient.status === 'Belum Terverifikasi'"
|
||||
color="primary-600"
|
||||
size="x-large"
|
||||
@click="$emit('verify', patient)"
|
||||
prepend-icon="mdi-qrcode-scan"
|
||||
variant="flat"
|
||||
class="action-btn"
|
||||
rounded="xl"
|
||||
>
|
||||
VERIFIKASI
|
||||
</v-btn>
|
||||
|
||||
<v-chip
|
||||
v-else
|
||||
color="secondary-600"
|
||||
size="x-large"
|
||||
variant="flat"
|
||||
class="verified-chip"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon start size="24">mdi-shield-check</v-icon>
|
||||
VERIFIED
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="patients.length === 0">
|
||||
<v-list-item-title class="empty-state">
|
||||
{{ emptyMessage }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
patients: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: 'Tidak ada data pasien'
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['verify']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-400: #E5F7FA;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-700: #717171;
|
||||
$neutral-900: #212121;
|
||||
$primary-100: #FFE8CC;
|
||||
$primary-200: #FFDCAF;
|
||||
$primary-600: #FFA532;
|
||||
$secondary-200: #EDF5FF;
|
||||
$secondary-300: #DBEDFF;
|
||||
$secondary-400: #B3D9FF;
|
||||
$secondary-600: #0671E0;
|
||||
$secondary-700: #0053AD;
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
$font-weight-extra-bold: 800;
|
||||
|
||||
.patient-item {
|
||||
border-bottom: 1px solid $neutral-400;
|
||||
padding: 24px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.patient-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background: $primary-600;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.patient-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.patient-item:hover {
|
||||
background: $primary-100 !important;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.verified-item {
|
||||
background: $secondary-200 !important;
|
||||
}
|
||||
|
||||
.verified-item::before {
|
||||
background: $secondary-600 !important;
|
||||
}
|
||||
|
||||
.verified-item:hover {
|
||||
background: $secondary-300 !important;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid $neutral-100;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 24px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
color: $neutral-900;
|
||||
margin-bottom: 8px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 14px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.chip-rm {
|
||||
border: 1px solid $secondary-400;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.chip-rm-text {
|
||||
color: $secondary-700;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
color: $neutral-700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
text-transform: none;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
font-size: 16px;
|
||||
color: $neutral-100;
|
||||
padding: 12px 32px;
|
||||
box-shadow: 0 2px 8px rgba(255, 155, 27, 0.25);
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.verified-chip {
|
||||
font-weight: $font-weight-extra-bold;
|
||||
font-size: 16px;
|
||||
color: $neutral-100;
|
||||
padding: 12px 24px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 0;
|
||||
font-size: 18px;
|
||||
color: $neutral-600;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.patient-item {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
max-width="600"
|
||||
transition="dialog-bottom-transition"
|
||||
scrollable
|
||||
>
|
||||
<v-card class="modal-card">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<v-icon size="72" color="white" class="mb-3">mdi-qrcode-scan</v-icon>
|
||||
<h2 class="modal-title">Aktivasi Akun</h2>
|
||||
<p class="modal-subtitle">{{ patient.nama }}</p>
|
||||
<v-chip color="white" class="modal-chip" variant="flat" size="default">
|
||||
<span class="modal-chip-text">RM: {{ patient.rm }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<v-card-text class="pa-7">
|
||||
<v-text-field
|
||||
:model-value="phoneNumber"
|
||||
@update:model-value="$emit('update:phoneNumber', $event)"
|
||||
:disabled="qrGenerated"
|
||||
label="Nomor Telepon Pasien"
|
||||
placeholder="08xxxxxxxxxx"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:color="qrGenerated ? 'grey' : 'primary-600'"
|
||||
:rules="[v => v.length >= 8 || 'Min. 8 digit']"
|
||||
prepend-inner-icon="mdi-phone"
|
||||
class="mb-3 phone-field"
|
||||
base-color="grey-darken-1"
|
||||
/>
|
||||
|
||||
<!-- Generate Button -->
|
||||
<v-btn
|
||||
v-if="!qrGenerated"
|
||||
:disabled="!isPhoneValid"
|
||||
color="secondary-600"
|
||||
block
|
||||
size="x-large"
|
||||
class="generate-btn"
|
||||
@click="$emit('generate')"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-icon left size="32">mdi-qrcode-plus</v-icon>
|
||||
Generate QR Code
|
||||
</v-btn>
|
||||
|
||||
<!-- QR Code Container -->
|
||||
<div v-else class="qr-container">
|
||||
<div class="pulse-icon">
|
||||
<v-icon color="secondary-600" :size="isMobile ? 48 : 64">
|
||||
mdi-cellphone-check
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<h3 class="qr-title">Pindai QR Code</h3>
|
||||
<p class="qr-subtitle">Arahkan kamera smartphone ke QR code</p>
|
||||
|
||||
<div class="d-flex justify-center mb-3 mb-sm-4">
|
||||
<div class="qr-frame">
|
||||
<slot name="qr-code" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<v-card-actions v-if="qrGenerated" class="modal-actions">
|
||||
<v-btn
|
||||
color="secondary-600"
|
||||
variant="outlined"
|
||||
@click="$emit('reload')"
|
||||
prepend-icon="mdi-reload"
|
||||
size="x-large"
|
||||
class="modal-action-btn"
|
||||
rounded="xl"
|
||||
>
|
||||
Reload QR
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
@click="$emit('complete')"
|
||||
prepend-icon="mdi-check-circle"
|
||||
size="x-large"
|
||||
class="modal-action-btn-primary"
|
||||
rounded="xl"
|
||||
>
|
||||
Selesai
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-card-actions v-else class="modal-actions">
|
||||
<v-btn
|
||||
color="neutral-600"
|
||||
variant="text"
|
||||
@click="$emit('close')"
|
||||
size="x-large"
|
||||
block
|
||||
rounded="xl"
|
||||
class="modal-cancel-btn"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
patient: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
phoneNumber: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
qrGenerated: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'update:phoneNumber',
|
||||
'generate',
|
||||
'reload',
|
||||
'complete',
|
||||
'close'
|
||||
]);
|
||||
|
||||
const isPhoneValid = computed(() => props.phoneNumber.length >= 8);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-700: #717171;
|
||||
$primary-600: #FFA532;
|
||||
$secondary-200: #EDF5FF;
|
||||
$secondary-600: #0671E0;
|
||||
$secondary-700: #0053AD;
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
$font-weight-extra-bold: 800;
|
||||
$font-weight-black: 900;
|
||||
|
||||
.modal-card {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
|
||||
text-align: center;
|
||||
padding: 28px;
|
||||
box-shadow: 0 4px 12px rgba(6, 99, 199, 0.2);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32px;
|
||||
font-weight: $font-weight-black;
|
||||
color: $neutral-100;
|
||||
margin: 8px 0;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-size: 20px;
|
||||
color: $neutral-100;
|
||||
opacity: 0.95;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin: 0;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.modal-chip {
|
||||
margin-top: 8px;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.modal-chip-text {
|
||||
color: $primary-600;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.phone-field {
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
margin-top: 12px;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
font-size: 18px;
|
||||
color: $neutral-100;
|
||||
box-shadow: 0 2px 8px rgba(6, 99, 199, 0.25);
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background: $secondary-200;
|
||||
border: 3px solid $secondary-600;
|
||||
border-radius: 16px;
|
||||
padding: 28px 16px;
|
||||
margin-top: 24px;
|
||||
box-shadow: 0 6px 20px rgba(6, 99, 199, 0.15);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.qr-container {
|
||||
padding: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
font-size: 24px;
|
||||
font-weight: $font-weight-black;
|
||||
color: $secondary-700;
|
||||
margin-bottom: 8px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.qr-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-subtitle {
|
||||
font-size: 13px;
|
||||
color: $neutral-700;
|
||||
margin-bottom: 16px;
|
||||
font-weight: $font-weight-semibold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.qr-subtitle {
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-frame {
|
||||
padding: 16px;
|
||||
background: $neutral-100;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
display: inline-block;
|
||||
border: 3px solid $secondary-600;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.qr-frame {
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
border: 4px solid $secondary-600;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: 28px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.modal-action-btn,
|
||||
.modal-action-btn-primary {
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
flex-grow: 1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.modal-action-btn-primary {
|
||||
color: $neutral-100;
|
||||
font-weight: $font-weight-extra-bold;
|
||||
}
|
||||
|
||||
.modal-cancel-btn {
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<v-row class="search-section">
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-text-field
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="$emit('update:searchQuery', $event)"
|
||||
density="comfortable"
|
||||
label="Cari pasien berdasarkan nama atau RM..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
single-line
|
||||
color="primary-600"
|
||||
bg-color="white"
|
||||
class="search-field"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="py-2">
|
||||
<v-tabs
|
||||
:model-value="selectedTab"
|
||||
@update:model-value="$emit('update:selectedTab', $event)"
|
||||
color="primary-600"
|
||||
align-tabs="end"
|
||||
class="filter-tabs"
|
||||
slider-color="primary-600"
|
||||
>
|
||||
<v-tab :value="0" class="tab-item">
|
||||
SEMUA
|
||||
<v-chip size="small" color="primary-600" class="ml-2 chip-count">
|
||||
{{ allCount }}
|
||||
</v-chip>
|
||||
</v-tab>
|
||||
|
||||
<v-tab :value="1" class="tab-item">
|
||||
PENDING
|
||||
<v-chip size="small" color="primary-600" class="ml-2 chip-count">
|
||||
{{ pendingCount }}
|
||||
</v-chip>
|
||||
</v-tab>
|
||||
|
||||
<v-tab :value="2" class="tab-item">
|
||||
VERIFIED
|
||||
<v-chip size="small" color="primary-600" class="ml-2 chip-count">
|
||||
{{ verifiedCount }}
|
||||
</v-chip>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectedTab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
allCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
pendingCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
verifiedCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['update:searchQuery', 'update:selectedTab']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$neutral-100: #FFFFFF;
|
||||
$neutral-500: #ABBED1;
|
||||
$neutral-600: #89939E;
|
||||
$neutral-300: #F5F7FA;
|
||||
$primary-600: #FFA532;
|
||||
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
.search-section {
|
||||
background: $neutral-300;
|
||||
border-bottom: 1px solid $neutral-500;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
border-radius: 8px;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
background: transparent;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
|
||||
.chip-count {
|
||||
color: #000 !important;
|
||||
font-weight: $font-weight-bold;
|
||||
font-family: $font-family-base;
|
||||
}
|
||||
</style>
|
||||
+20
-12
@@ -19,15 +19,23 @@ export default defineNuxtConfig({
|
||||
"@nuxt/test-utils",
|
||||
"@nuxt/ui",
|
||||
"@pinia/nuxt",
|
||||
"@vesp/nuxt-fontawesome",
|
||||
"@nuxtjs/google-fonts",
|
||||
(_options, nuxt) => {
|
||||
nuxt.hooks.hook("vite:extendConfig", (config) => {
|
||||
// @ts-expect-error
|
||||
config.plugins.push(vuetify({ autoImport: true }));
|
||||
});
|
||||
});
|
||||
},
|
||||
],
|
||||
|
||||
fontawesome: {
|
||||
icons: {
|
||||
solid: ["dna", "user", "home", "gear"],
|
||||
regular: ["heart"],
|
||||
brands: ["github"],
|
||||
},
|
||||
},
|
||||
googleFonts: {
|
||||
families: {
|
||||
Inter: [400, 500, 600, 700],
|
||||
@@ -41,8 +49,8 @@ export default defineNuxtConfig({
|
||||
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||
keycloakIssuer: process.env.KEYCLOAK_ISSUER,
|
||||
public: {
|
||||
authUrl: process.env.AUTH_ORIGIN || 'http://localhost:3001'
|
||||
}
|
||||
authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001",
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
@@ -54,10 +62,10 @@ export default defineNuxtConfig({
|
||||
"@mdi/font/css/materialdesignicons.min.css",
|
||||
"~/assets/scss/main.scss",
|
||||
],
|
||||
// devServer: {
|
||||
// host: "10.10.150.114",
|
||||
// port: 3001,
|
||||
// },
|
||||
devServer: {
|
||||
host: "10.10.150.114",
|
||||
port: 3001,
|
||||
},
|
||||
|
||||
// 'http://10.10.150.114:3001/'
|
||||
|
||||
@@ -65,18 +73,18 @@ export default defineNuxtConfig({
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
api: "modern-compiler",
|
||||
additionalData: `
|
||||
@use "sass:math";
|
||||
@use "sass:map";
|
||||
@use "~/assets/scss/_variables.scss" as *;
|
||||
@use "~/assets/scss/_colors.scss" as *;
|
||||
`
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
noExternal: ["vuetify"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+91
-21
@@ -7,6 +7,9 @@
|
||||
"name": "nuxt-app",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@nuxt/content": "^2.7.2",
|
||||
"@nuxt/eslint": "^1.7.1",
|
||||
@@ -16,6 +19,7 @@
|
||||
"@nuxt/ui": "^3.3.0",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@unhead/vue": "^2.0.13",
|
||||
"@vesp/nuxt-fontawesome": "^2.0.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"dayjs": "^1.11.18",
|
||||
@@ -1460,6 +1464,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
|
||||
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
|
||||
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz",
|
||||
"integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",
|
||||
"integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1916,9 +1977,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxt/devtools-kit": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-3.0.0.tgz",
|
||||
"integrity": "sha512-/X8GLPYydj/Lbmti7M+HGPeeBCfzzZnAXtERjnbUqQsPNUmRMDt1nOSPHYN4NL6TaH3ECHt3HYqJt/O+Hw2kDg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-3.1.0.tgz",
|
||||
"integrity": "sha512-1AEZS6ge8G9X3sJauw6hTWqTpUIVqs5Uq9d7Z9cjUAinXjE+pGliVQ+i8xWCNnGLaZCCSqX/I/M/EByD3v2JIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^4.2.1",
|
||||
@@ -6014,6 +6075,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vesp/nuxt-fontawesome": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vesp/nuxt-fontawesome/-/nuxt-fontawesome-2.0.0.tgz",
|
||||
"integrity": "sha512-20of3+NQHy5naZIk/DsH4LKjfmjEyiA1ajvrrCZexqEkVuf9e8IBVvtl228zAdzSgRxjHDcWGLKW8j0sirCDzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
@@ -7191,9 +7261,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.20",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
|
||||
"integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==",
|
||||
"version": "2.8.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
|
||||
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -7223,9 +7293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
|
||||
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz",
|
||||
"integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
@@ -7537,9 +7607,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001751",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -8635,9 +8705,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.241",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz",
|
||||
"integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==",
|
||||
"version": "1.5.262",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
|
||||
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/embla-carousel": {
|
||||
@@ -16314,9 +16384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/srvx": {
|
||||
"version": "0.8.16",
|
||||
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.8.16.tgz",
|
||||
"integrity": "sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ==",
|
||||
"version": "0.9.6",
|
||||
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.9.6.tgz",
|
||||
"integrity": "sha512-5L4rT6qQqqb+xcoDoklUgCNdmzqJ6vbcDRwPVGRXewF55IJH0pqh0lQlrJ266ZWTKJ4mfeioqHQJeAYesS+RrQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"srvx": "bin/srvx.mjs"
|
||||
@@ -18179,9 +18249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-vue-tracer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-tracer/-/vite-plugin-vue-tracer-1.0.1.tgz",
|
||||
"integrity": "sha512-L5/vAhT6oYbH4RSQYGLN9VfHexWe7SGzca1pJ7oPkL6KtxWA1jbGeb3Ri1JptKzqtd42HinOq4uEYqzhVWrzig==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-tracer/-/vite-plugin-vue-tracer-1.1.3.tgz",
|
||||
"integrity": "sha512-fM7hfHELZvbPnSn8EKZwHfzxm5EfYFQIclz8rwcNXfodNbRkwNvh0AGMtaBfMxQ9HC5KVa3KitwHnmE4ezDemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"estree-walker": "^3.0.3",
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@nuxt/content": "^2.7.2",
|
||||
"@nuxt/eslint": "^1.7.1",
|
||||
@@ -21,6 +24,7 @@
|
||||
"@nuxt/ui": "^3.3.0",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@unhead/vue": "^2.0.13",
|
||||
"@vesp/nuxt-fontawesome": "^2.0.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"dayjs": "^1.11.18",
|
||||
|
||||
+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>
|
||||
+296
-8
@@ -1,4 +1,4 @@
|
||||
// stores/masterStore.js
|
||||
// stores/masterStore.js - Integrated dengan Penunjang
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
@@ -355,6 +355,258 @@ export const useMasterStore = defineStore('master', () => {
|
||||
return allRuang;
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// MASTER PENUNJANG
|
||||
// ============================================
|
||||
const penunjangData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
kode: 'LAB',
|
||||
nama: 'LABORATORIUM',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Pemeriksaan darah lengkap, urine, dan pemeriksaan laboratorium lainnya'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
kode: 'RAD',
|
||||
nama: 'RADIOLOGI',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 100,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 45,
|
||||
keterangan: 'Rontgen, CT Scan, MRI, USG, dan pemeriksaan radiologi lainnya'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
kode: 'PAT',
|
||||
nama: 'PATOLOGI ANATOMI',
|
||||
jenis: 'Medis',
|
||||
shift: 1,
|
||||
totalQuota: 50,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 50 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 60,
|
||||
keterangan: 'Pemeriksaan jaringan dan sitologi'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
kode: 'RHB',
|
||||
nama: 'REHABILITASI MEDIK',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 80,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 40 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Fisioterapi, terapi okupasi, dan terapi wicara'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
no: 5,
|
||||
kode: 'RME',
|
||||
nama: 'REKAM MEDIS',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 120,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 70 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 15,
|
||||
keterangan: 'Pengurusan dokumen rekam medis dan surat keterangan'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
no: 6,
|
||||
kode: 'GIZ',
|
||||
nama: 'GIZI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 60,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 30 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 30 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Konsultasi gizi dan diet'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
no: 7,
|
||||
kode: 'LND',
|
||||
nama: 'LAUNDRY',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '06:00', sampai: '14:00', kuota: 80 },
|
||||
{ dari: '14:00', sampai: '22:00', kuota: 70 },
|
||||
{ dari: '22:00', sampai: '06:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Layanan laundry linen rumah sakit'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
no: 8,
|
||||
kode: 'INF',
|
||||
nama: 'PELAYANAN INFORMASI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Informasi umum rumah sakit dan bantuan navigasi'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
no: 9,
|
||||
kode: 'FRM',
|
||||
nama: 'FARMASI',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Pelayanan obat dan konsultasi farmasi'
|
||||
}
|
||||
]);
|
||||
|
||||
// Computed - Penunjang
|
||||
const totalPenunjang = computed(() => penunjangData.value.length);
|
||||
const activePenunjang = computed(() => penunjangData.value.filter(p => p.status === 'Aktif'));
|
||||
const penunjangMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Medis'));
|
||||
const penunjangNonMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Non-Medis'));
|
||||
|
||||
// Get penunjang list for dropdowns
|
||||
const penunjangList = computed(() =>
|
||||
penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
kode: p.kode,
|
||||
nama: p.nama
|
||||
}))
|
||||
);
|
||||
|
||||
// Actions - Penunjang
|
||||
const addPenunjang = (penunjangPayload) => {
|
||||
const newId = Math.max(...penunjangData.value.map(p => p.id), 0) + 1;
|
||||
const newNo = penunjangData.value.length + 1;
|
||||
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
const newPenunjang = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
|
||||
penunjangData.value.push(newPenunjang);
|
||||
return { success: true, message: `Penunjang ${newPenunjang.nama} berhasil ditambahkan`, data: newPenunjang };
|
||||
};
|
||||
|
||||
const updatePenunjang = (penunjangPayload) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangPayload.id);
|
||||
if (index !== -1) {
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
penunjangData.value[index] = {
|
||||
...penunjangData.value[index],
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
return { success: true, message: `Penunjang ${penunjangPayload.nama} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deletePenunjang = (penunjangId) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangId);
|
||||
if (index !== -1) {
|
||||
const penunjangName = penunjangData.value[index].nama;
|
||||
penunjangData.value.splice(index, 1);
|
||||
penunjangData.value.forEach((p, idx) => {
|
||||
p.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Penunjang ${penunjangName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getPenunjangById = (id) => {
|
||||
return penunjangData.value.find(p => p.id === id);
|
||||
};
|
||||
|
||||
const getPenunjangByKode = (kode) => {
|
||||
return penunjangData.value.find(p => p.kode === kode);
|
||||
};
|
||||
|
||||
// Get active penunjang list (untuk Admin Penunjang)
|
||||
const getActivePenunjangList = () => {
|
||||
return penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({ id: p.id, name: p.nama, kode: p.kode }));
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================
|
||||
@@ -363,41 +615,77 @@ export const useMasterStore = defineStore('master', () => {
|
||||
return klinik ? klinik.nama : kode;
|
||||
};
|
||||
|
||||
const getPenunjangNameByKode = (kode) => {
|
||||
const penunjang = penunjangData.value.find(p => p.kode === kode);
|
||||
return penunjang ? penunjang.nama : kode;
|
||||
};
|
||||
|
||||
return {
|
||||
// State - Klinik
|
||||
// ============================================
|
||||
// KLINIK
|
||||
// ============================================
|
||||
// State
|
||||
klinikData,
|
||||
klinikList,
|
||||
|
||||
// Actions - Klinik
|
||||
// Actions
|
||||
addKlinik,
|
||||
updateKlinik,
|
||||
deleteKlinik,
|
||||
getKlinikById,
|
||||
getKlinikByKode,
|
||||
|
||||
// State - Loket
|
||||
// ============================================
|
||||
// LOKET
|
||||
// ============================================
|
||||
// State
|
||||
loketData,
|
||||
availableServices,
|
||||
|
||||
// Actions - Loket
|
||||
// Actions
|
||||
addLoket,
|
||||
updateLoket,
|
||||
deleteLoket,
|
||||
getLoketById,
|
||||
|
||||
// State - Ruang
|
||||
// ============================================
|
||||
// RUANG
|
||||
// ============================================
|
||||
// State
|
||||
ruangData,
|
||||
totalKlinikRuang,
|
||||
totalRuangan,
|
||||
getAllRuangList,
|
||||
|
||||
// Actions - Ruang
|
||||
// Actions
|
||||
addRuang,
|
||||
updateRuang,
|
||||
deleteRuang,
|
||||
getRuangByKlinik,
|
||||
|
||||
// Utilities
|
||||
// ============================================
|
||||
// PENUNJANG
|
||||
// ============================================
|
||||
// State
|
||||
penunjangData,
|
||||
totalPenunjang,
|
||||
activePenunjang,
|
||||
penunjangMedis,
|
||||
penunjangNonMedis,
|
||||
penunjangList,
|
||||
|
||||
// Actions
|
||||
addPenunjang,
|
||||
updatePenunjang,
|
||||
deletePenunjang,
|
||||
getPenunjangById,
|
||||
getPenunjangByKode,
|
||||
getActivePenunjangList,
|
||||
|
||||
// ============================================
|
||||
// UTILITIES
|
||||
// ============================================
|
||||
getKlinikNameByKode,
|
||||
getPenunjangNameByKode,
|
||||
};
|
||||
});
|
||||
+2
-1
@@ -52,7 +52,8 @@ const defaultNavItems: NavItem[] = [
|
||||
{ id: 17, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
|
||||
{ id: 18, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
|
||||
{ id: 19, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
|
||||
{ id: 20, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
|
||||
{ id: 20, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
|
||||
{ id: 21, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
+108
-4
@@ -12,7 +12,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
noAntrian: "UM1001 | Online - 250811100163",
|
||||
shift: "Shift 1",
|
||||
klinik: "KANDUNGAN",
|
||||
fastTrack: "UMUM",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting", // waiting, di-loket, terlambat, pending, processed
|
||||
processStage: "loket", // loket, klinik, penunjang
|
||||
@@ -25,7 +25,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
noAntrian: "UM1002 | Online - 250811100155",
|
||||
shift: "Shift 1",
|
||||
klinik: "IPD",
|
||||
fastTrack: "UMUM",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
@@ -38,7 +38,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
noAntrian: "UM1003 | Online - 250811100200",
|
||||
shift: "Shift 1",
|
||||
klinik: "SARAF",
|
||||
fastTrack: "UMUM",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
@@ -51,7 +51,111 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
noAntrian: "UM1004 | Online - 250811100210",
|
||||
shift: "Shift 1",
|
||||
klinik: "THT",
|
||||
fastTrack: "UMUM",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 5,
|
||||
jamPanggil: "12:49",
|
||||
barcode: "250811100163",
|
||||
noAntrian: "UM1001 | Online - 250811100163",
|
||||
shift: "Shift 1",
|
||||
klinik: "KANDUNGAN",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting", // waiting, di-loket, terlambat, pending, processed
|
||||
processStage: "loket", // loket, klinik, penunjang
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 6,
|
||||
jamPanggil: "10:52",
|
||||
barcode: "250811100155",
|
||||
noAntrian: "UM1002 | Online - 250811100155",
|
||||
shift: "Shift 1",
|
||||
klinik: "IPD",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 7,
|
||||
jamPanggil: "09:30",
|
||||
barcode: "250811100200",
|
||||
noAntrian: "UM1003 | Online - 250811100200",
|
||||
shift: "Shift 1",
|
||||
klinik: "SARAF",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 8,
|
||||
jamPanggil: "14:15",
|
||||
barcode: "250811100210",
|
||||
noAntrian: "UM1004 | Online - 250811100210",
|
||||
shift: "Shift 1",
|
||||
klinik: "THT",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 9,
|
||||
jamPanggil: "12:49",
|
||||
barcode: "250811100163",
|
||||
noAntrian: "UM1001 | Online - 250811100163",
|
||||
shift: "Shift 1",
|
||||
klinik: "KANDUNGAN",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting", // waiting, di-loket, terlambat, pending, processed
|
||||
processStage: "loket", // loket, klinik, penunjang
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 10,
|
||||
jamPanggil: "10:52",
|
||||
barcode: "250811100155",
|
||||
noAntrian: "UM1002 | Online - 250811100155",
|
||||
shift: "Shift 1",
|
||||
klinik: "IPD",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 11,
|
||||
jamPanggil: "09:30",
|
||||
barcode: "250811100200",
|
||||
noAntrian: "UM1003 | Online - 250811100200",
|
||||
shift: "Shift 1",
|
||||
klinik: "SARAF",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
no: 12,
|
||||
jamPanggil: "14:15",
|
||||
barcode: "250811100210",
|
||||
noAntrian: "UM1004 | Online - 250811100210",
|
||||
shift: "Shift 1",
|
||||
klinik: "THT",
|
||||
fastTrack: "TIDAK",
|
||||
pembayaran: "UMUM",
|
||||
status: "waiting",
|
||||
processStage: "loket",
|
||||
|
||||
Reference in New Issue
Block a user