668 lines
19 KiB
Vue
668 lines
19 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Header -->
|
|
<div class="page-header">
|
|
<div class="header-content">
|
|
<div class="header-left">
|
|
<div class="header-icon">
|
|
<v-icon size="28" 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>
|
|
</div>
|
|
|
|
<v-container>
|
|
<!-- Action Bar -->
|
|
<div class="action-bar mb-4">
|
|
<div class="action-bar-left">
|
|
<v-chip color="primary" variant="tonal" class="stat-chip mr-2">
|
|
<v-icon start size="16">mdi-account-group</v-icon>
|
|
{{ totalCount }} Total
|
|
</v-chip>
|
|
<v-chip color="warning" variant="tonal" class="stat-chip mr-2">
|
|
<v-icon start size="16">mdi-clock-outline</v-icon>
|
|
{{ waitingCount }} Menunggu
|
|
</v-chip>
|
|
<v-chip color="success" variant="tonal" class="stat-chip">
|
|
<v-icon start size="16">mdi-check-circle</v-icon>
|
|
{{ doneCount }} Selesai
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<v-card>
|
|
<!-- Status Filter Section -->
|
|
<v-card-text class="filter-section-top">
|
|
<div class="section-label">DATA PASIEN</div>
|
|
|
|
<v-chip-group
|
|
v-model="activeTab"
|
|
mandatory
|
|
class="status-filter"
|
|
>
|
|
<v-chip
|
|
value="all"
|
|
:class="{ 'active-chip': activeTab === 'all' }"
|
|
@click="filterByStatus('all')"
|
|
>
|
|
<v-icon start size="16">mdi-account-group</v-icon>
|
|
Semua Pasien ({{ totalCount }})
|
|
</v-chip>
|
|
<v-chip
|
|
value="Menunggu Poli"
|
|
:class="{ 'active-chip': activeTab === 'Menunggu Poli' }"
|
|
@click="filterByStatus('Menunggu Poli')"
|
|
>
|
|
<v-icon start size="16">mdi-clock-outline</v-icon>
|
|
Menunggu Poli ({{ waitingCount }})
|
|
</v-chip>
|
|
<v-chip
|
|
value="Diperiksa Dokter"
|
|
:class="{ 'active-chip': activeTab === 'Diperiksa Dokter' }"
|
|
@click="filterByStatus('Diperiksa Dokter')"
|
|
>
|
|
<v-icon start size="16">mdi-doctor</v-icon>
|
|
Diperiksa Dokter ({{ examiningCount }})
|
|
</v-chip>
|
|
<v-chip
|
|
value="Selesai Pelayanan"
|
|
:class="{ 'active-chip': activeTab === 'Selesai Pelayanan' }"
|
|
@click="filterByStatus('Selesai Pelayanan')"
|
|
>
|
|
<v-icon start size="16">mdi-check-circle</v-icon>
|
|
Selesai Pelayanan ({{ doneCount }})
|
|
</v-chip>
|
|
</v-chip-group>
|
|
</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"
|
|
prepend-inner-icon="mdi-card-account-details"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
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"
|
|
prepend-inner-icon="mdi-numeric"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
type="number"
|
|
clearable
|
|
class="input-field"
|
|
></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" sm="3">
|
|
<v-select
|
|
label="Layanan"
|
|
v-model="filters.service"
|
|
:items="['Klinik Jiwa', 'Radiologi', 'Fisioterapi', 'Klinik Umum']"
|
|
prepend-inner-icon="mdi-hospital-building"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
class="input-field"
|
|
></v-select>
|
|
</v-col>
|
|
<v-col cols="12" sm="2">
|
|
<v-btn
|
|
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-col cols="12" sm="2">
|
|
<v-btn
|
|
block
|
|
height="40"
|
|
@click="resetFilters"
|
|
class="btn-reset"
|
|
variant="outlined"
|
|
>
|
|
<v-icon left size="18">mdi-refresh</v-icon>
|
|
Reset
|
|
</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.no="{ index }">
|
|
{{ index + 1 }}
|
|
</template>
|
|
|
|
<template v-slot:item.status="{ item }">
|
|
<v-chip
|
|
size="small"
|
|
:class="getStatusChipClass(item.status)"
|
|
>
|
|
<v-icon
|
|
:icon="getStatusIcon(item.status)"
|
|
size="14"
|
|
class="mr-1"
|
|
></v-icon>
|
|
{{ 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>
|
|
<div class="pa-8 text-center">
|
|
<v-icon size="48" color="grey-lighten-1" class="mb-2">mdi-information-outline</v-icon>
|
|
<p class="text-body-1 text-grey-darken-1">Tidak ada data pasien untuk ditampilkan.</p>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-container>
|
|
</div>
|
|
</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', key: 'no', sortable: false, width: '60px' },
|
|
{ title: 'No. RM', key: 'rm_number', sortable: true },
|
|
{ title: 'Nama', key: 'name', sortable: true },
|
|
{ title: 'No. Antrean', key: 'queue_number', sortable: true, width: '120px' },
|
|
{ title: 'Layanan', key: 'service', sortable: true },
|
|
{ title: 'Status', key: 'status', sortable: true, width: '180px' },
|
|
{ title: 'Aksi', key: 'aksi', sortable: false, width: '120px' },
|
|
];
|
|
|
|
// 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';
|
|
};
|
|
|
|
// Reset all filters
|
|
const resetFilters = () => {
|
|
filters.value.rm_number = '';
|
|
filters.value.qr_code = '';
|
|
filters.value.queue_number = '';
|
|
filters.value.service = null;
|
|
currentSearchTerm.value = '';
|
|
filteredPatientList.value = ALL_PATIENTS_DATA;
|
|
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 chip class based on status
|
|
const getStatusChipClass = (status) => {
|
|
if (status === 'Diperiksa Dokter') return 'chip-success';
|
|
if (status === 'Menunggu Poli') return 'chip-warning';
|
|
if (status === 'Selesai Pelayanan') return 'chip-primary';
|
|
return 'chip-neutral';
|
|
};
|
|
|
|
// Utility function to set icon based on status
|
|
const getStatusIcon = (status) => {
|
|
if (status === 'Diperiksa Dokter') return 'mdi-doctor';
|
|
if (status === 'Menunggu Poli') return 'mdi-clock-outline';
|
|
if (status === 'Selesai Pelayanan') return 'mdi-check-circle';
|
|
return 'mdi-help-circle';
|
|
};
|
|
|
|
// 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;
|
|
|
|
$primary-700: #3556AE;
|
|
$primary-600: #3A61C9;
|
|
|
|
$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, $primary-600 0%, $primary-700 100%);
|
|
border-radius: 0 !important;
|
|
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
|
|
}
|
|
|
|
.header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 28px;
|
|
height: 80px;
|
|
color: $neutral-100;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-icon {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
margin-right: 16px;
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 32px;
|
|
line-height: 40px;
|
|
font-weight: $font-weight-semibold;
|
|
margin: 0;
|
|
color: $neutral-100;
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.page-subtitle {
|
|
margin: 2px 0 0 0;
|
|
opacity: 0.9;
|
|
font-size: 15px;
|
|
line-height: 22px;
|
|
font-weight: $font-weight-regular;
|
|
color: $neutral-100;
|
|
}
|
|
|
|
.header-stats {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stat-chip {
|
|
color: $primary-600 !important;
|
|
font-weight: $font-weight-semibold;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
// ============================================
|
|
// ACTION BAR
|
|
// ============================================
|
|
.action-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
background: $neutral-100;
|
|
border-radius: 12px;
|
|
border: 1px solid $neutral-400;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.action-bar-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
// ============================================
|
|
// FILTER SECTION TOP
|
|
// ============================================
|
|
.filter-section-top {
|
|
padding: 24px 24px 16px 24px !important;
|
|
background: $neutral-100;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
color: $neutral-600;
|
|
text-transform: uppercase;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.status-filter {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.status-filter .v-chip {
|
|
font-size: 12px;
|
|
font-weight: $font-weight-semibold;
|
|
height: 32px;
|
|
border: 1px solid $neutral-500;
|
|
background: $neutral-100;
|
|
color: $neutral-600;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
background: $neutral-300;
|
|
}
|
|
}
|
|
|
|
.status-filter .v-chip.active-chip {
|
|
background: $primary-600;
|
|
color: $neutral-100;
|
|
border-color: $primary-600;
|
|
}
|
|
|
|
// ============================================
|
|
// FILTER SECTION
|
|
// ============================================
|
|
.filter-section {
|
|
padding: 0 24px 24px 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: $primary-600 !important;
|
|
color: $neutral-100 !important;
|
|
text-transform: none;
|
|
font-weight: $font-weight-semibold;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
|
|
&:hover {
|
|
background-color: $primary-700 !important;
|
|
}
|
|
}
|
|
|
|
.btn-reset {
|
|
border-color: $neutral-600 !important;
|
|
color: $neutral-800 !important;
|
|
text-transform: none;
|
|
font-weight: $font-weight-semibold;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
|
|
&:hover {
|
|
background-color: $neutral-300 !important;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// DATA TABLE
|
|
// ============================================
|
|
.data-table {
|
|
font-family: $font-family-base;
|
|
|
|
:deep(.v-data-table__th) {
|
|
font-weight: $font-weight-semibold;
|
|
color: $neutral-800;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
:deep(.v-data-table__td) {
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
color: $neutral-900;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// CHIPS
|
|
// ============================================
|
|
.chip-success {
|
|
background-color: $success-600 !important;
|
|
color: $neutral-100 !important;
|
|
font-weight: $font-weight-medium;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.chip-warning {
|
|
background-color: #F59E0B !important;
|
|
color: $neutral-100 !important;
|
|
font-weight: $font-weight-medium;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.chip-primary {
|
|
background-color: $primary-600 !important;
|
|
color: $neutral-100 !important;
|
|
font-weight: $font-weight-medium;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
}
|
|
|
|
.chip-neutral {
|
|
background-color: $neutral-600 !important;
|
|
color: $neutral-100 !important;
|
|
font-weight: $font-weight-medium;
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
}
|
|
|
|
// ============================================
|
|
// BUTTONS
|
|
// ============================================
|
|
.btn-view {
|
|
background-color: $primary-600 !important;
|
|
color: $neutral-100 !important;
|
|
text-transform: none;
|
|
font-weight: $font-weight-semibold;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
|
|
&:hover {
|
|
background-color: $primary-700 !important;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// RESPONSIVE
|
|
// ============================================
|
|
@media (max-width: 768px) {
|
|
.header-content {
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.header-stats {
|
|
width: 100%;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-card {
|
|
padding: 16px;
|
|
}
|
|
}
|
|
</style> |