Merge branch 'Antrean-Code' of https://git.rssa.top/arie.bagus.2905/web-antrean into tes-refaktoring

This commit is contained in:
Fanrouver
2026-07-14 09:41:38 +07:00
2 changed files with 354 additions and 226 deletions
+352 -219
View File
@@ -20,16 +20,13 @@ const profileData = reactive({
registrationNumber: 'RM-2023-8812',
nim: '3573230897851332',
phone: '+62 812-3456-7890',
verified: true
verified: true,
birthDate: '10 Juni 1970',
address: 'Jalan Soekarno Hatta, no 1A, Lowokwaru, Kota Malang'
});
// Family members data
const familyMembers = reactive([
{
id: 1,
name: 'Andi Pratama',
status: 'PENDING'
},
{
id: 2,
name: 'Siti Aminah',
@@ -81,54 +78,179 @@ const modalData = reactive({
alamat: ''
});
const genderOptions = ['Laki-laki', 'Perempuan'];
const relationshipOptions = ['Orang tua', 'Suami/Istri', 'Anak', 'Saudara', 'Lainnya'];
const openModal = (member) => {
selectedMember.value = member;
// Reset form
modalData.namaLengkap = member.name;
modalData.tanggalLahir = '';
modalData.nik = '';
modalData.jenisKelamin = '';
modalData.hubungan = '';
modalData.nomorTelepon = '';
modalData.alamat = '';
isModalOpen.value = true;
/** Opsi hubungan anggota keluarga */
const relationshipOptions = [
'Anak Kandung',
'Anak Tiri',
'Anak Angkat',
'Suami',
'Istri',
'Orang Tua',
'Saudara Kandung',
'Wali',
'Kerabat',
'Lainnya'
];
// ── State Modal Tambah Anggota ──────────────────────────────────────────────
/** Mode modal: 'add' untuk tambah baru, 'edit' untuk edit data yang ada */
const memberModalMode = ref('add');
/** Apakah modal tambah/edit anggota terbuka */
const isAddMemberModalOpen = ref(false);
/** Judul modal yang ditampilkan sesuai mode */
const memberModalTitle = computed(() => {
if (memberModalMode.value === 'editProfile') return 'Edit Profil';
if (memberModalMode.value === 'edit') return 'Edit Peserta';
return 'Tambah Peserta';
});
/** Label tombol submit modal sesuai mode */
const memberModalSubmitLabel = computed(() =>
memberModalMode.value === 'add' ? 'Tambah' : 'Simpan'
);
/** True jika tombol submit harus disabled */
const memberModalSubmitDisabled = computed(() => {
if (!searchResult.value) return true;
// Mode editProfile: cukup ada searchResult (tidak perlu hubungan)
if (memberModalMode.value === 'editProfile') return false;
// Mode tambah/edit anggota: wajib pilih hubungan
return !addMemberForm.hubungan;
});
/** Teks pencarian pasien (nomor RM atau NIK) */
const searchQuery = ref('');
/** Loading saat sedang mencari pasien */
const isSearching = ref(false);
/** Hasil pencarian / data pasien yang sedang diedit; null = belum ada data */
const searchResult = ref(null);
/** Form data tambah/edit anggota — hanya hubungan & alamat yang dapat diisi */
const addMemberForm = reactive({
hubungan: '',
alamat: ''
});
/**
* Membuka modal dalam mode TAMBAH dan mereset semua state pencarian.
*/
const openAddMemberModal = () => {
memberModalMode.value = 'add';
searchQuery.value = '';
searchResult.value = null;
addMemberForm.hubungan = '';
addMemberForm.alamat = '';
isAddMemberModalOpen.value = true;
};
const closeModal = () => {
isModalOpen.value = false;
selectedMember.value = null;
};
const handleApprove = () => {
snackbar.value = {
show: true,
message: 'Pengajuan anggota baru disetujui.',
color: 'success'
/**
* Membuka modal dalam mode EDIT PROFIL, mengisi data dari profileData.
* Section pencarian dan field Hubungan disembunyikan.
* Field Nomor Telepon & Alamat menjadi enabled.
*/
const openEditProfileModal = () => {
memberModalMode.value = 'editProfile';
searchResult.value = {
namaLengkap: profileData.name,
nomorRM: profileData.registrationNumber,
tanggalLahir: profileData.birthDate,
nik: profileData.nim,
jenisKelamin: 'Laki-laki',
// Nomor Telepon dimasukkan ke field yang dapat diedit
nomorTelepon: profileData.phone
};
closeModal();
// Alamat profil dimasukkan ke addMemberForm.alamat agar bisa diedit
addMemberForm.alamat = profileData.address;
addMemberForm.hubungan = '';
isAddMemberModalOpen.value = true;
};
const handleReject = () => {
snackbar.value = {
show: true,
message: 'Pengajuan anggota baru ditolak.',
color: 'error'
/**
* Membuka modal dalam mode EDIT, langsung mengisi data pasien.
* Section pencarian disembunyikan saat mode ini.
* @param {object} member - Data anggota yang akan diedit
*/
const openEditMemberModal = (member) => {
memberModalMode.value = 'edit';
// Isi langsung data dari member ke searchResult (sama strukturnya)
searchResult.value = {
namaLengkap: member.name,
nomorRM: member.registrationNumber ?? '-',
tanggalLahir: member.birthDate ?? '-',
nik: member.nik ?? '-',
jenisKelamin: member.jenisKelamin ?? '-',
nomorTelepon: member.phone ?? '-'
};
closeModal();
addMemberForm.hubungan = member.hubungan ?? '';
addMemberForm.alamat = member.address ?? '';
isAddMemberModalOpen.value = true;
};
/**
* Menutup modal tambah/edit anggota.
*/
const closeAddMemberModal = () => {
isAddMemberModalOpen.value = false;
};
/**
* Mensimulasikan pencarian pasien berdasarkan nomor RM atau NIK.
* Pada implementasi nyata, ini akan memanggil API backend.
*/
const handleSearchPatient = async () => {
if (!searchQuery.value.trim()) return;
isSearching.value = true;
searchResult.value = null;
// Simulasi delay network request
await new Promise(resolve => setTimeout(resolve, 800));
// Data dummy hasil pencarian pasien
searchResult.value = {
namaLengkap: 'Andi Pratama',
nomorRM: searchQuery.value,
tanggalLahir: '12 Maret 1970',
nik: '197054478954315',
jenisKelamin: 'Laki-laki',
nomorTelepon: '0812-3456-7890'
};
isSearching.value = false;
};
/**
* Menangani submit form tambah/edit anggota / edit profil.
* Pesan snackbar menyesuaikan mode yang aktif.
*/
const handleAddMember = () => {
const nama = searchResult.value?.namaLengkap;
let message = '';
if (memberModalMode.value === 'editProfile') {
// Update profileData langsung dengan nilai yang sudah diubah user
profileData.phone = searchResult.value.nomorTelepon;
profileData.address = addMemberForm.alamat;
message = 'Profil berhasil diperbarui.';
} else if (memberModalMode.value === 'edit') {
message = `Data anggota ${nama} berhasil diperbarui.`;
} else {
message = `Anggota keluarga ${nama} berhasil ditambahkan.`;
}
snackbar.value = { show: true, message, color: 'success' };
closeAddMemberModal();
};
</script>
<template>
<div>
<PageHeader
icon="mdi-shield-check"
title="Detail Akun"
:subtitle="currentDate"
:show-add-button="false"
theme="primary"
/>
<div style="background: var(--color-neutral-300);">
<PageHeader icon="mdi-shield-check" title="Detail Akun" :subtitle="currentDate" :show-add-button="false"
theme="primary" />
<v-container class="py-6">
@@ -136,54 +258,94 @@ const handleReject = () => {
<!-- Profile Card -->
<v-row class="mb-6">
<v-col cols="12">
<v-card class="pa-8" color="white" elevation="2" rounded="lg">
<v-row class="align-center" no-gutters>
<v-card class="pa-6" color="white" elevation="2" rounded="lg">
<!-- Baris 1: Avatar + Nama + Tombol Edit -->
<v-row class="align-center mb-2" no-gutters>
<!-- Avatar -->
<v-col cols="auto" class="mr-6">
<v-card height="100" width="100" class="bg-lightPrimary d-flex align-center justify-center" elevation="0" rounded="lg">
<v-icon size="50" color="primary">mdi-account-outline</v-icon>
<v-col cols="auto" class="mr-5">
<v-card height="100" width="100" class="bg-lightPrimary d-flex align-center justify-center"
elevation="0" rounded="lg">
<v-icon size="40" color="primary">mdi-account-outline</v-icon>
</v-card>
</v-col>
<!-- Nama Lengkap & NIK -->
<v-col cols="3">
<div class="text-caption text-uppercase text-grey font-weight-bold">Nama Lengkap</div>
<div class="text-h6 font-weight-bold text-primary-700 mb-4">{{ profileData.name }}</div>
<div class="text-caption text-uppercase text-grey font-weight-bold">N.I.K</div>
<div class="text-body2">{{ profileData.nim }}</div>
</v-col>
<v-col cols="max">
<!-- Baris 2: NIK | Nomor RM | Nomor Telepon -->
<v-row no-gutters class="mb-0">
<v-col cols="12" class="mb-3">
<v-row>
<!-- Nama Lengkap -->
<v-col>
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Nama Lengkap
</div>
<div class="text-h6 font-weight-bold text-primary">
{{ profileData.name }}
</div>
</v-col>
<!-- Nomor RM & Nomor Telepon -->
<v-col cols="3">
<div class="text-caption text-uppercase text-grey font-weight-bold">Nomor RM</div>
<div class="text-h6 font-weight-bold mb-4">{{ profileData.registrationNumber }}</div>
<div class="text-caption text-uppercase text-grey font-weight-bold">Nomor Telepon</div>
<div class="text-body2">{{ profileData.phone }}</div>
</v-col>
<!-- Tombol Edit Profil -->
<v-col cols="auto">
<v-btn color="primary" variant="outlined" size="small" rounded="lg"
@click="openEditProfileModal">
<v-icon start size="small">mdi-pencil</v-icon>
Edit Profil
</v-btn>
</v-col>
</v-row>
</v-col>
<v-col cols="4">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
NIK
</div>
<div class="text-body-1 font-weight-bold">{{ profileData.nim }}</div>
</v-col>
<!-- Buttons -->
<v-col cols="max" class="text-right">
<v-chip
v-if="profileData.verified"
color="success"
size="small"
class="mb-3"
>
<v-icon start size="small">mdi-check-circle</v-icon>
Terverifikasi
</v-chip>
<div>
<v-btn
color="primary"
variant="outlined"
size="small"
>
<v-icon start>mdi-pencil</v-icon>
Edit Profil
</v-btn>
</div>
<v-col cols="4">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Nomor RM
</div>
<div class="text-body-1 font-weight-bold">{{ profileData.registrationNumber }}</div>
</v-col>
<v-col cols="4">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Nomor Telepon
</div>
<div class="text-body-1 font-weight-bold">{{ profileData.phone }}</div>
</v-col>
</v-row>
</v-col>
</v-row>
<v-divider class="my-4" />
<!-- Baris 3: Tanggal Lahir | Nomor Telepon (No RM) | Alamat -->
<v-row no-gutters>
<v-col cols="2">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Tanggal Lahir
</div>
<div class="text-body-2">{{ profileData.birthDate }}</div>
</v-col>
<v-col cols="3">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Nomor Telepon
</div>
<div class="text-body-2">{{ profileData.registrationNumber }}</div>
</v-col>
<v-col cols="7">
<div class="text-caption text-uppercase text-grey-darken-1 font-weight-bold mb-1">
Alamat
</div>
<div class="text-body-2">{{ profileData.address }}</div>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
@@ -191,9 +353,16 @@ const handleReject = () => {
<!-- Family Members Section -->
<v-row class="mb-6">
<v-col cols="12">
<h3 class="text-h6 font-weight-bold">Anggota Keluarga Terhubung</h3>
<!-- Header: judul + tombol Tambah Anggota -->
<div class="d-flex align-center justify-space-between mb-1">
<h3 class="text-h6 font-weight-bold">Anggota Keluarga Terhubung</h3>
<v-btn color="primary" variant="flat" size="small" rounded="lg" @click="openAddMemberModal">
<v-icon start size="small">mdi-account-plus</v-icon>
Tambah Anggota
</v-btn>
</div>
<p class="text-body-2 text-muted mb-4">Daftar anggota keluarga yang berada dalam satu kartu keluarga</p>
<v-card class="pa-2">
<v-table class="elevation-0">
<thead>
@@ -207,10 +376,7 @@ const handleReject = () => {
<tr v-for="member in familyMembers" :key="member.id">
<td class="text-body-2 font-weight-bold ">{{ member.name }}</td>
<td class="text-center">
<v-chip
:color="member.status === 'PENDING' ? 'secondary' : 'success'"
size="small"
>
<v-chip :color="member.status === 'PENDING' ? 'secondary' : 'success'" size="small">
<v-icon start size="small">{{
member.status === 'PENDING' ? 'mdi-clock-outline' : 'mdi-check-decagram'
}}</v-icon>
@@ -219,30 +385,12 @@ const handleReject = () => {
</td>
<td class="text-center">
<div class="d-flex justify-center gap-2">
<v-btn
v-if="member.status === 'PENDING'"
size="small"
color="primary"
@click="openModal(member)"
>
<v-icon start>mdi-clipboard-check-outline</v-icon>
Proses
</v-btn>
<div v-else>
<v-btn
size="small"
color="error"
variant="outlined"
class="mr-2"
>
<div>
<v-btn size="small" color="error" variant="outlined" class="mr-2">
<v-icon start>mdi-delete</v-icon>
Hapus
</v-btn>
<v-btn
size="small"
color="primary"
variant="outlined"
>
<v-btn size="small" color="primary" variant="outlined" @click="openEditMemberModal(member)">
<v-icon start>mdi-pencil</v-icon>
Edit
</v-btn>
@@ -260,26 +408,19 @@ const handleReject = () => {
<v-row>
<v-col cols="12">
<div class="d-flex align-center mb-6">
<v-icon class="mr-2" color="primary">mdi-history</v-icon>
<h3 class="text-h6 font-weight-bold">Riwayat Aktivitas</h3>
</div>
<v-icon class="mr-2" color="primary">mdi-history</v-icon>
<h3 class="text-h6 font-weight-bold">Riwayat Aktivitas</h3>
</div>
<v-card class="pa-6">
<v-timeline
density="compact"
side="end">
<v-timeline-item
v-for="activity in activityHistory"
:key="activity.id"
:dot-color="activity.color"
:icon="activity.icon"
fill-dot
>
<v-timeline density="compact" side="end">
<v-timeline-item v-for="activity in activityHistory" :key="activity.id" :dot-color="activity.color"
:icon="activity.icon" fill-dot>
<div class="text-subtitle-1 font-weight-bold">{{ activity.title }}</div>
<div class="text-caption text-muted">{{ activity.date }}</div>
</v-timeline-item>
</v-timeline>
</v-timeline>
<div class="text-center mt-6">
<v-btn color="primary" variant="text">
@@ -290,124 +431,116 @@ const handleReject = () => {
</v-col>
</v-row>
<!-- Verification Modal -->
<v-dialog v-model="isModalOpen" max-width="700px" persistent scrollable>
<!-- Modal Tambah Anggota Keluarga -->
<v-dialog v-model="isAddMemberModalOpen" max-width="700px" persistent scrollable>
<v-card class="pa-0">
<!-- Modal Header -->
<v-card-title class="bg-primary text-white pa-6">
<div class="d-flex justify-space-between align-center w-100">
<h2 class="text-h5 font-weight-bold">Verifikasi Pengajuan Anggota Keluarga</h2>
<v-btn icon="mdi-close" variant="text" @click="closeModal" class="text-white"></v-btn>
<h2 class="text-h5 font-weight-bold">{{ memberModalTitle }}</h2>
<v-btn icon="mdi-close" variant="text" @click="closeAddMemberModal" class="text-white"></v-btn>
</div>
</v-card-title>
<!-- Form Content - Scrollable -->
<v-card-text class="pa-6 bg-light">
<!-- Informasi Data Diri Section - Card -->
<v-card class="mb-6 pa-6" variant="flat">
<div class="d-flex align-center mb-4">
<h3 class="text-subtitle-1 font-weight-bold text-primary">INFORMASI DATA DIRI</h3>
</div>
<!-- Bagian: Data Pasien (Cari) hanya tampil saat mode tambah -->
<v-card v-if="memberModalMode === 'add'" class="mb-6 pa-6" variant="flat">
<div class="d-flex align-center mb-4">
<h3 class="text-subtitle-1 font-weight-bold text-primary">DATA PASIEN</h3>
</div>
<v-form>
<v-row>
<v-col cols="12">
<v-text-field
v-model="modalData.namaLengkap"
label="Nama Lengkap"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="modalData.tanggalLahir"
label="Tanggal Lahir"
type="date"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="modalData.jenisKelamin"
:items="genderOptions"
label="Jenis Kelamin"
variant="outlined"
density="compact"
></v-select>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="modalData.nik"
label="NIK"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="modalData.hubungan"
:items="relationshipOptions"
label="Hubungan"
variant="outlined"
density="compact"
></v-select>
<v-text-field v-model="searchQuery" placeholder="Cari nomor RM atau NIK" variant="outlined"
density="compact" hide-details :loading="isSearching" @keyup.enter="handleSearchPatient">
<template #prepend-inner>
<v-icon size="small" color="grey">mdi-magnify</v-icon>
</template>
<template #append-inner>
<v-btn color="primary" variant="text" size="small" icon="mdi-magnify" :loading="isSearching"
@click="handleSearchPatient" />
</template>
</v-text-field>
</v-col>
</v-row>
</v-form>
</v-card>
<!-- Kontak & Alamat Section - Card -->
<v-card class="mb-6 pa-6" variant="flat">
<div class="d-flex align-center mb-4">
<h3 class="text-subtitle-1 font-weight-bold text-primary">KONTAK & ALAMAT</h3>
</div>
<!-- Hasil Pencarian: tampil setelah ada hasil -->
<template v-if="searchResult">
<!-- Informasi Data Diri Section -->
<v-card class="mb-6 pa-6" variant="flat">
<div class="d-flex align-center mb-4">
<h3 class="text-subtitle-1 font-weight-bold text-primary">INFORMASI DATA DIRI</h3>
</div>
<v-form>
<v-row>
<v-col cols="12" sm="6">
<v-text-field v-model="searchResult.namaLengkap" label="Nama Lengkap" variant="outlined"
density="compact" disabled />
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="searchResult.nomorRM" label="Nomor RM" variant="outlined" density="compact"
disabled />
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="searchResult.tanggalLahir" label="Tanggal Lahir" variant="outlined"
density="compact" disabled />
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="searchResult.nik" label="NIK" variant="outlined" density="compact"
disabled />
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="searchResult.jenisKelamin" label="Jenis Kelamin" variant="outlined"
density="compact" disabled />
</v-col>
<!-- Hubungan: hanya tampil saat mode tambah/edit anggota -->
<v-col v-if="memberModalMode !== 'editProfile'" cols="12" sm="6">
<v-select v-model="addMemberForm.hubungan" :items="relationshipOptions" label="Hubungan"
variant="outlined" density="compact" />
</v-col>
</v-row>
</v-form>
</v-card>
<!-- Kontak & Alamat Section -->
<v-card class="mb-6 pa-6" variant="flat">
<div class="d-flex align-center mb-4">
<h3 class="text-subtitle-1 font-weight-bold text-primary">KONTAK & ALAMAT</h3>
</div>
<v-form>
<v-row>
<v-col cols="6">
<!-- Nomor Telepon: enabled saat editProfile, disabled saat mode lain -->
<v-text-field v-model="searchResult.nomorTelepon" label="Nomor Telepon" variant="outlined"
density="compact" :disabled="memberModalMode !== 'editProfile'" />
</v-col>
<v-col cols="6">
<!-- Alamat: freetext yang dapat diisi -->
<v-text-field v-model="addMemberForm.alamat" label="Alamat" variant="outlined"
density="compact" />
</v-col>
</v-row>
</v-form>
</v-card>
</template>
<v-form>
<v-row>
<v-col cols="12">
<v-text-field
v-model="modalData.nomorTelepon"
label="Nomor Telepon"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="modalData.alamat"
label="Alamat"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-form>
</v-card>
</v-card-text>
<!-- Modal Actions -->
<v-divider></v-divider>
<v-card-actions class="pa-6 justify-end">
<v-btn
color="error"
variant="outlined"
@click="handleReject"
>
<v-icon start>mdi-close</v-icon>
Tolak
</v-btn>
<v-btn
color="primary"
variant="flat"
@click="handleApprove"
>
<v-icon start>mdi-check</v-icon>
Setujui
<v-btn color="primary" variant="flat" :disabled="memberModalSubmitDisabled" @click="handleAddMember">
<v-icon start>mdi-check-circle</v-icon>
{{ memberModalSubmitLabel }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
+2 -7
View File
@@ -102,6 +102,7 @@
Verifikasi
</v-btn>
<v-btn
v-if="item.status !== 'Belum Terverifikasi'"
class="my-2"
color="primary"
size="small"
@@ -114,11 +115,6 @@
</template>
<template v-slot:item.pending="{ item }">
<v-chip size="small" class="chip-orange">
{{ item.pending || 2 }}
</v-chip>
</template>
</v-data-table>
</v-card-text>
</v-card>
@@ -179,10 +175,9 @@ const headers = ref([
{ title: 'No', value: 'no', sortable: false, width: '60px', align: 'center' },
{ title: 'Nama Pasien', value: 'nama', sortable: true, align: 'center' },
{ title: 'No. RM', value: 'rm', sortable: true, align: 'center' },
{ title: 'Alamat', value: 'alamat', sortable: false, align: 'left' },
{ title: 'Alamat', value: 'alamat', sortable: false, align: 'left', width: '400px' },
{ title: 'No. Telepon', value: 'telepon', sortable: false, align: 'center' },
{ title: 'Status', value: 'status', sortable: true, width: '180px', align: 'center' },
{ title: 'Pending', value: 'pending', sortable: false, width: '120px', align: 'center' },
{ title: 'Actions', value: 'actions', sortable: false, width: '200px', align: 'center' },
]);