Files
web-antrean/pages/MonitoringPasien/monitoringPasien.vue
T
2026-01-06 14:26:19 +07:00

470 lines
14 KiB
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-heart-pulse</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Monitoring Pasien</h2>
<p class="page-subtitle">{{ currentDate }} - Manajemen Data Pasien</p>
</div>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-account-group</v-icon>
{{ totalCount }} Total
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-clock-outline</v-icon>
{{ waitingCount }} Menunggu
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip">
<v-icon start size="16">mdi-check-circle</v-icon>
{{ doneCount }} Selesai
</v-chip>
</div>
</div>
</div>
<!-- Tabs -->
<v-card-text class="tabs-section">
<v-tabs
v-model="activeTab"
color="primary"
align-tabs="start"
show-arrows
class="status-tabs"
>
<v-tab value="all" @click="filterByStatus('all')">
Semua Pasien <v-badge color="grey-lighten-1" :content="totalCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Menunggu Poli" @click="filterByStatus('Menunggu Poli')">
Menunggu Poli <v-badge color="orange-lighten-1" :content="waitingCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Diperiksa Dokter" @click="filterByStatus('Diperiksa Dokter')">
Diperiksa Dokter <v-badge color="green-lighten-1" :content="examiningCount" inline class="ml-2"></v-badge>
</v-tab>
<v-tab value="Selesai Pelayanan" @click="filterByStatus('Selesai Pelayanan')">
Selesai Pelayanan <v-badge color="blue-lighten-1" :content="doneCount" inline class="ml-2"></v-badge>
</v-tab>
</v-tabs>
</v-card-text>
<!-- Filter Section -->
<v-card-text class="filter-section">
<div class="filter-card">
<v-row class="align-center">
<v-col cols="12" sm="3">
<v-text-field
label="No. RM"
v-model="filters.rm_number"
variant="outlined"
density="compact"
hide-details
class="input-field"
></v-text-field>
</v-col>
<v-col cols="12" sm="2">
<v-text-field
label="No. Antrean"
v-model="filters.queue_number"
variant="outlined"
density="compact"
hide-details
type="number"
class="input-field"
></v-text-field>
</v-col>
<v-col cols="12" sm="2">
<v-select
label="Layanan"
v-model="filters.service"
:items="['Klinik Jiwa', 'Radiologi', 'Fisioterapi', 'Klinik Umum']"
variant="outlined"
density="compact"
hide-details
clearable
class="input-field"
></v-select>
</v-col>
<v-col cols="12" sm="2">
<v-btn
color="success"
block
height="40"
@click="applyFilters"
class="btn-search"
variant="flat"
>
<v-icon left size="18">mdi-magnify</v-icon>
Cari
</v-btn>
</v-col>
</v-row>
</div>
</v-card-text>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="paginatedPatients"
:items-per-page="10"
class="elevation-0 data-table"
:search="currentSearchTerm"
>
<template v-slot:item.status="{ item }">
<v-chip :color="getStatusColor(item.status)" size="small" class="font-weight-medium status-chip" label>
{{ item.status }}
</v-chip>
</template>
<template v-slot:item.aksi="{ item }">
<v-btn
size="small"
@click="viewPatient(item.id)"
class="btn-view"
variant="flat"
>
<v-icon size="16" left>mdi-eye</v-icon>
Lihat
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="info" icon="mdi-information-outline">
Tidak ada data pasien untuk ditampilkan.
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
definePageMeta({
middleware:['auth']
})
const router = useRouter();
// Get current date formatted
const currentDate = computed(() => {
const date = new Date();
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
return `${days[date.getDay()]}, ${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
});
// === STATE MANAGEMENT ===
const activeTab = ref('all');
// Filter Model: Menampung input dari form pencarian
const filters = ref({
rm_number: '',
// qr_code: '',
queue_number: '', // Filter baru
service: null, // Filter Layanan
});
// State untuk menyimpan hasil filtering
const filteredPatientList = ref([]);
const currentSearchTerm = ref('');
// Table Headers
const headers = [
{ title: 'No. RM', key: 'rm_number' },
{ title: 'Nama', key: 'name' },
{ title: 'No. Antrean', key: 'queue_number' },
{ title: 'Layanan', key: 'service' },
{ title: 'Status', key: 'status' },
{ title: 'Aksi', key: 'aksi', sortable: false },
];
// Mock Patient Data
const ALL_PATIENTS_DATA = [
{ id: '11111111', rm_number: '11111111', name: 'Ragil Bayu N.', queue_number: 1, service: 'Klinik Jiwa', status: 'Diperiksa Dokter', qr_code: 'QR123' },
{ id: '22222222', rm_number: '22222222', name: 'Siti Aisyah', queue_number: 5, service: 'Radiologi', status: 'Menunggu Poli', qr_code: 'QR456' },
{ id: '33333333', rm_number: '33333333', name: 'Ahmad Dani', queue_number: 2, service: 'Klinik Jiwa', status: 'Selesai Pelayanan', qr_code: 'QR789' },
{ id: '44444444', rm_number: '44444444', name: 'Dewi Sartika', queue_number: 6, service: 'Fisioterapi', status: 'Menunggu Poli', qr_code: 'QR101' },
{ id: '55555555', rm_number: '55555555', name: 'Joko Susilo', queue_number: 3, service: 'Klinik Umum', status: 'Diperiksa Dokter', qr_code: 'QR112' },
{ id: '66666666', rm_number: '66666666', name: 'Budi Santoso', queue_number: 4, service: 'Radiologi', status: 'Menunggu Poli', qr_code: 'QR131' },
];
// Initialize the filtered list with all data
filteredPatientList.value = [...ALL_PATIENTS_DATA];
// === FILTERING LOGIC ===
// 1. Logic Pencarian Gabungan saat tombol "Cari" ditekan
const applyFilters = () => {
// 1. Filter berdasarkan teks di semua kolom yang relevan (RM, QR, Antrean, Nama)
let textSearch = '';
// Gabungkan semua nilai filter menjadi satu string pencarian
if (filters.value.rm_number) textSearch += filters.value.rm_number.toLowerCase() + ' ';
if (filters.value.qr_code) textSearch += filters.value.qr_code.toLowerCase() + ' ';
if (filters.value.queue_number) textSearch += filters.value.queue_number.toString().toLowerCase() + ' ';
// Vuetify v-data-table memiliki fitur pencarian bawaan (di properti `search`).
// Kita gunakan fitur ini untuk pencarian berbasis teks (No. RM, QR, Antrean).
currentSearchTerm.value = textSearch.trim();
// 2. Filter berdasarkan Layanan (Service), yang tidak didukung oleh `v-data-table` search
let results = ALL_PATIENTS_DATA;
if (filters.value.service) {
results = results.filter(p => p.service === filters.value.service);
}
// Terapkan hasil filter Layanan
filteredPatientList.value = results;
// Reset status tab (penting agar hasil pencarian tampil terlepas dari tab yang aktif)
activeTab.value = 'all';
};
// 2. Logic Filter Status (saat tab ditekan)
const filterByStatus = (status) => {
// Pastikan status tab diperbarui
activeTab.value = status;
// Clear the text search term when switching tabs
currentSearchTerm.value = '';
if (status === 'all') {
filteredPatientList.value = ALL_PATIENTS_DATA;
} else {
filteredPatientList.value = ALL_PATIENTS_DATA.filter(p => p.status === status);
}
// Clear form filters (opsional, tetapi membuat UX lebih jelas)
filters.value.rm_number = '';
filters.value.qr_code = '';
filters.value.queue_number = '';
filters.value.service = null;
};
// === COMPUTED PROPERTIES ===
// Hitungan untuk Badge Tab
const listForCounts = computed(() => {
// Gunakan ALL_PATIENTS_DATA untuk hitungan badge agar hitungan selalu stabil
return ALL_PATIENTS_DATA;
});
const totalCount = computed(() => listForCounts.value.length);
const waitingCount = computed(() => listForCounts.value.filter(p => p.status === 'Menunggu Poli').length);
const examiningCount = computed(() => listForCounts.value.filter(p => p.status === 'Diperiksa Dokter').length);
const doneCount = computed(() => listForCounts.value.filter(p => p.status === 'Selesai Pelayanan').length);
// Data yang ditampilkan di tabel adalah data yang sudah difilter
const paginatedPatients = computed(() => {
// Jika ada filter status yang aktif (setelah menekan tab)
if (activeTab.value !== 'all') {
return filteredPatientList.value.filter(p => p.status === activeTab.value);
}
// Jika tidak ada filter status yang aktif, tampilkan hasil dari applyFilters()
return filteredPatientList.value;
});
// === UTILITIES & NAVIGATION ===
// Utility function to set color based on status
const getStatusColor = (status) => {
if (status === 'Diperiksa Dokter') return 'green-darken-1';
if (status === 'Menunggu Poli') return 'orange-darken-1';
if (status === 'Selesai Pelayanan') return 'blue-darken-2';
return 'grey';
};
// Function to navigate to the detailed patient information page
const viewPatient = (patientId) => {
router.push(`/MonitoringPasien/Pasien/${patientId}`);
};
</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;
$success-700: #1B6E53;
$success-600: #009262;
$success-400: #32C997;
$success-300: #84DFC1;
$success-200: #F1FBF8;
$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;
// Apply font family
* {
font-family: $font-family-base;
}
// ============================================
// PAGE HEADER
// ============================================
.page-header {
background: linear-gradient(135deg, $success-600 0%, $success-700 100%);
border-radius: 16px 16px 0 0;
box-shadow: 0 4px 16px rgba(0, 146, 98, 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;
}
.header-stats {
display: flex;
align-items: center;
gap: 8px;
}
.stat-chip {
color: $success-600 !important;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
// ============================================
// TABS SECTION
// ============================================
.tabs-section {
padding: 24px 24px 0 24px !important;
background: $neutral-100;
}
.status-tabs {
font-family: $font-family-base;
}
.v-tab.v-tab--selected {
font-weight: $font-weight-semibold;
}
// ============================================
// FILTER SECTION
// ============================================
.filter-section {
padding: 24px !important;
background: $neutral-100;
}
.filter-card {
background: $neutral-300;
padding: 20px;
border-radius: 12px;
border: 1px solid $neutral-400;
}
.input-field {
font-size: 14px;
line-height: 20px;
}
.btn-search {
background-color: $success-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
// ============================================
// DATA TABLE
// ============================================
.data-table {
font-family: $font-family-base;
}
.status-chip {
font-weight: $font-weight-medium;
font-size: 12px;
line-height: 16px;
}
.btn-view {
background-color: $success-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
// ============================================
// RESPONSIVE
// ============================================
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 16px;
}
.header-stats {
width: 100%;
flex-wrap: wrap;
}
.filter-card {
padding: 16px;
}
}
</style>