feat(FE) : layouting dashboard,antrean,pendaftaran

This commit is contained in:
Yusron alamsyah
2026-01-26 09:35:16 +07:00
parent 65d4864013
commit cbd5dcf8f5
29 changed files with 2892 additions and 391 deletions

No files matched your search

@@ -151,44 +151,8 @@ const sessionInfo = computed(() => {
</div>
</div>
</div>
<v-divider></v-divider>
</div>
<PerfectScrollbar style="height: calc(100vh - 240px); max-height: 240px">
<v-list class="py-0 theme-list" lines="two">
<v-list-item
v-for="item in profileDD"
:key="item.title"
class="py-4 px-8 custom-text-primary"
:to="item.href"
>
<template v-slot:prepend>
<v-avatar
size="48"
color="lightprimary"
class="mr-3"
rounded="md"
>
<img
:src="item.avatar"
width="24"
height="24"
:alt="item.avatar"
/>
</v-avatar>
</template>
<div>
<h6 class="text-subtitle-1 font-weight-bold mb-2 custom-title">
{{ item.title }}
</h6>
</div>
<p class="text-subtitle-1 font-weight-regular textSecondary">
{{ item.subtitle }}
</p>
</v-list-item>
</v-list>
</PerfectScrollbar>
<!-- **DIPERBAIKI: Logout buttons section dengan multiple options** -->
<div class="pt-4 pb-6 px-8">
<!-- Regular Logout Button -->
@@ -203,24 +167,7 @@ const sessionInfo = computed(() => {
Logout
</v-btn>
<!-- **TAMBAHAN: Full Logout Button** -->
<v-btn
color="error"
variant="outlined"
block
@click="fullLogout"
prepend-icon="mdi-logout-variant"
class="mb-2"
>
Full Logout
</v-btn>
<!-- **TAMBAHAN: Quick info text** -->
<div class="text-center mt-2">
<span class="text-caption textSecondary">
Full logout akan menghapus semua sesi dan data lokal
</span>
</div>
</div>
</v-sheet>
</v-menu>
@@ -44,13 +44,13 @@ watch(priority, (newPriority) => {
<!-- ---------------------------------------------- -->
<!-- Search part -->
<!-- ---------------------------------------------- -->
<Searchbar />
<!-- <Searchbar /> -->
<!-- ---------------------------------------------- -->
<!-- Mega menu -->
<!-- ---------------------------------------------- -->
<div class="hidden-sm-and-down">
<Navigations />
<!-- <Navigations /> -->
</div>
<v-spacer class="hidden-sm-and-down" />
@@ -85,7 +85,7 @@ watch(priority, (newPriority) => {
<!-- Notification -->
<!-- ---------------------------------------------- -->
<div class="hidden-sm-and-down">
<NotificationDD />
<!-- <NotificationDD /> -->
</div>
<!-- ---------------------------------------------- -->
@@ -51,60 +51,7 @@ const sidebarMenu = shallowRef(sidebarItems);
<template>
<!-- Minisidebar Icons -->
<v-navigation-drawer
class="bg-light"
v-model="customizer.Sidebar_drawer"
top="0"
rail
rail-width="80"
>
<!-- <perfect-scrollbar class="miniscrollnavbar"> -->
<v-list-item class="px-0">
<!-- Toggle Sidebar Button -->
<div class="px-4 mb-3">
<v-btn
class="hidden-md-and-down my-2"
icon
rounded="md"
variant="plain"
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)"
>
<Icon icon="solar:hamburger-menu-line-duotone" height="25" />
</v-btn>
</div>
<div class="miniicons mt-lg-0 mt-4">
<!-- MiniSidebar Icons -->
<div class="d-flex flex-column gap-2">
<div
class="miniicons-list px-4"
v-for="menu in MiniSideIcons"
:key="menu.icon"
>
<v-btn
rounded="md"
flat
icon
variant="plain"
@click="showData(menu.id)"
:class="{ 'bg-primary opacity-1': currentMenu === menu.id }"
>
<Icon :icon="'solar:' + menu.icon" width="25" />
<!-- Tooltip on Hover -->
<v-tooltip
activator="parent"
location="end"
class="custom-tooltip"
>{{ menu.tooltip }}</v-tooltip
>
</v-btn>
</div>
</div>
</div>
</v-list-item>
<!-- </perfect-scrollbar> -->
</v-navigation-drawer>
<!-- LeftSidebar Items -->
<v-navigation-drawer
@@ -21,209 +21,29 @@ const sidebarItem: menu[] = [
id: 1,
children: [
{
title: 'Dashboard1',
title: 'Dashboard',
icon: 'widget-add-line-duotone',
to: '/dashboards/dashboard1'
},
{
title: 'Dashboard2',
icon: 'chart-line-duotone',
to: '/dashboards/dashboard2'
},
{
title: 'Dashboard3',
icon: 'screencast-2-line-duotone',
to: '/dashboards/dashboard3'
},
{
title: 'Front Pages',
icon: 'home-angle-linear',
to: '/',
children: [
{
title: 'Homepage',
to: '/front-page/homepage'
},
{
title: 'About Us',
to: '/front-page/about-us'
},
{
title: 'Blog',
to: '/front-page/blog/posts'
},
{
title: 'Blog Details',
to: '/front-page/blog/early-black-friday-amazon-deals-cheap-tvs-headphones'
},
{
title: 'Contact Us',
to: '/front-page/contact-us'
},
{
title: 'Portfolio',
to: '/front-page/portfolio'
},
{
title: 'Pricing',
to: '/front-page/pricing'
}
]
},
to: '/'
}
]
},
{
header: 'apps',
header: 'antrean',
id: 1,
children: [
{
title: 'ECommerce',
title: 'Semua',
icon: 'cart-3-line-duotone',
to: '/ecommerce/',
children: [
{
title: 'Shop',
to: '/ecommerce/products'
},
{
title: 'Detail',
to: '/ecommerce/product/detail/1'
},
{
title: 'List',
to: '/ecommerce/productlist'
},
{
title: 'Checkout',
to: '/ecommerce/checkout'
},
{
title: 'Add Product',
to: '/ecommerce/add-product'
},
{
title: 'Edit Product',
to: '/ecommerce/edit-product'
}
]
to: '/antrean/all',
},
{
title: 'Blog',
title: 'Spesialis',
icon: 'widget-4-line-duotone',
to: '/',
children: [
{
title: 'Blog Posts',
to: '/apps/blog/posts'
},
{
title: 'Blog Details',
to: '/apps/blog/early-black-friday-amazon-deals-cheap-tvs-headphones'
}
]
},
{
title: 'User Profile',
icon: 'shield-user-line-duotone',
to: '/',
children: [
{
title: 'Profile',
to: '/apps/user/profile'
},
{
title: 'Followers',
to: '/apps/user/profile/followers'
},
{
title: 'Friends',
to: '/apps/user/profile/friends'
},
{
title: 'Gallery',
to: '/apps/user/profile/gallery'
}
]
},
{
title: 'Invoice',
icon: 'bill-check-outline',
to: '/',
children: [
{
title: 'List',
to: '/apps/invoice'
},
{
title: 'Details',
to: '/apps/invoice/details/102'
},
{
title: 'Create',
to: '/apps/invoice/create'
},
{
title: 'Edit',
to: '/apps/invoice/edit/102'
}
]
},
{
title: 'Calendar',
icon: 'calendar-mark-line-duotone',
to: '/apps/calendar'
},
{
title: 'Email',
icon: 'letter-linear',
to: '/apps/email'
},
{
title: 'Chats',
icon: 'chat-round-line-line-duotone',
to: '/apps/chats'
},
{
title: 'Notes',
icon: 'document-text-line-duotone',
to: '/apps/notes'
},
{
title: 'Kanban',
icon: 'airbuds-case-minimalistic-line-duotone',
to: '/apps/kanban'
},
{
title: 'Contact',
icon: 'iphone-line-duotone',
to: '/apps/contacts'
},
{
title: 'Tickets',
icon: 'ticker-star-outline',
to: '/apps/tickets'
},
to: '/antrean/list-spesialis'
}
]
},
{
header: 'Master',
id: 2,
children: [
]
},
{
header: 'Forms',
id: 3,
children: [
]
},
}
];
export default sidebarItem;
+248
View File
@@ -0,0 +1,248 @@
<script setup lang="ts">
const formData = defineModel<{
noRekamMedis: string;
noKtp: string;
namaPasien: string;
jenisKelamin: string;
tanggalLahir: string;
umur: string;
alamat: string;
nomorTelepon: string[];
}>({ required: true });
const rules = {
required: (value: string) => !!value || 'Field ini wajib diisi'
};
// Modal state
const showModal = ref(false);
const newPhoneNumber = ref('');
const editingIndex = ref<number | null>(null);
// Initialize nomorTelepon as array if not already
if (!Array.isArray(formData.value.nomorTelepon)) {
formData.value.nomorTelepon = [];
}
const openAddModal = () => {
newPhoneNumber.value = '';
editingIndex.value = null;
showModal.value = true;
};
const editPhoneNumber = (index: number) => {
const phone = formData.value.nomorTelepon[index];
// Remove +62 prefix for editing
newPhoneNumber.value = phone.startsWith('+62') ? phone.substring(3) : phone;
editingIndex.value = index;
showModal.value = true;
};
// Handle only numeric input
const handlePhoneInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
// Only allow numbers
const numericValue = value.replace(/\D/g, '');
newPhoneNumber.value = numericValue;
};
const savePhoneNumber = () => {
if (!newPhoneNumber.value.trim()) return;
const fullPhoneNumber = `+62${newPhoneNumber.value}`;
if (editingIndex.value !== null) {
formData.value.nomorTelepon[editingIndex.value] = fullPhoneNumber;
} else {
formData.value.nomorTelepon.push(fullPhoneNumber);
}
showModal.value = false;
newPhoneNumber.value = '';
editingIndex.value = null;
};
const deletePhoneNumber = (index: number) => {
formData.value.nomorTelepon.splice(index, 1);
};
</script>
<template>
<v-card elevation="10">
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Biodata Pasien</h5>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-row>
<!-- No Rekam Medis -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">No Rekam Medis <span
class="text-error">*</span></v-label>
<v-text-field
v-model="formData.noRekamMedis"
placeholder="Masukan Nomor RM / Nama Pasien"
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
></v-text-field>
</v-col>
<!-- No KTP -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">No Ktp</v-label>
<v-text-field v-model="formData.noKtp" variant="outlined" density="comfortable"
hide-details="auto" readonly bg-color="grey-lighten-3"></v-text-field>
</v-col>
<!-- Nama Pasien -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Nama Pasien</v-label>
<v-text-field v-model="formData.namaPasien" variant="outlined" density="comfortable"
hide-details="auto" readonly bg-color="grey-lighten-3"></v-text-field>
</v-col>
<!-- Jenis Kelamin -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Jenis Kelamin</v-label>
<v-radio-group v-model="formData.jenisKelamin" inline hide-details="auto">
<v-radio label="Laki-Laki" value="L" color="primary"></v-radio>
<v-radio label="Perempuan" value="P" color="primary"></v-radio>
</v-radio-group>
</v-col>
<!-- Tanggal Lahir -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Tanggal Lahir</v-label>
<v-text-field v-model="formData.tanggalLahir" type="date" variant="outlined"
density="comfortable" hide-details="auto" readonly
bg-color="grey-lighten-3"></v-text-field>
</v-col>
<!-- Umur -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Umur</v-label>
<v-text-field v-model="formData.umur" variant="outlined" density="comfortable"
hide-details="auto" readonly bg-color="grey-lighten-3"></v-text-field>
</v-col>
<!-- Alamat -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Alamat</v-label>
<v-textarea v-model="formData.alamat" variant="outlined" rows="3" hide-details="auto"
readonly bg-color="grey-lighten-3"></v-textarea>
</v-col>
<!-- Nomor Telepon -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Nomor Telepon</v-label>
<!-- Alert when no phone numbers -->
<v-alert
v-if="formData.nomorTelepon.length === 0"
type="info"
variant="tonal"
class="mb-3"
density="compact"
>
<div class="d-flex align-center">
<span>No items</span>
</div>
</v-alert>
<!-- List of phone numbers -->
<v-list v-if="formData.nomorTelepon.length > 0" class="mb-3" density="compact">
<v-list-item
v-for="(phone, index) in formData.nomorTelepon"
:key="index"
class="px-4 mb-2"
border
@click="editPhoneNumber(index)"
style="cursor: pointer;"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<span>{{ phone }}</span>
<v-btn
icon
size="small"
variant="text"
color="error"
@click.stop="deletePhoneNumber(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
<!-- Add Button -->
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-plus"
@click="openAddModal"
>
Tambah Nomor Telepon
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Modal for adding phone number -->
<v-dialog v-model="showModal" max-width="600px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-package-variant</v-icon>
<span class="text-h6">{{ editingIndex !== null ? 'Edit' : 'Tambah' }} Item Data Telepon Pasien Operasi</span>
</div>
<v-btn
icon
variant="text"
@click="showModal = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-6">
<v-label class="mb-2 font-weight-medium">Nomor Telepon</v-label>
<v-text-field
v-model="newPhoneNumber"
placeholder="Masukan nomor telepon lengkap"
variant="outlined"
density="comfortable"
hide-details="auto"
type="tel"
@input="handlePhoneInput"
>
<template #prepend-inner>
<span class="text-medium-emphasis">+62</span>
</template>
</v-text-field>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
variant="flat"
@click="savePhoneNumber"
:disabled="!newPhoneNumber.trim()"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
+422
View File
@@ -0,0 +1,422 @@
<script setup lang="ts">
import diagnosisData from '~/data/mock/diagnosa.json';
import tindakanData from '~/data/mock/tindakan.json';
interface DiagnosisItem {
kodeDiagnosa: string;
diagnosa: string;
jenisDiagnosa: string;
}
interface TindakanItem {
kodeTindakan: string;
tindakan: string;
tindakanTambahan?: string;
}
const diagnosisItems = defineModel<DiagnosisItem[]>('diagnosisItems', { required: true });
const tindakanItems = defineModel<TindakanItem[]>('tindakanItems', { required: true });
// Modal state for diagnosis
const showDiagnosisModal = ref(false);
const diagnosisForm = ref<DiagnosisItem>({
kodeDiagnosa: '',
diagnosa: '',
jenisDiagnosa: 'utama'
});
const editingDiagnosisIndex = ref<number | null>(null);
const diagnosisList = ref(diagnosisData);
// Autocomplete options for diagnosis
const diagnosisOptions = computed(() => {
return diagnosisList.value.map(d => ({
title: `${d.kode} - ${d.diagnosa}`,
value: d.kode,
diagnosa: d.diagnosa,
jenisDiagnosa: d.jenis_diagnosa
}));
});
// Modal state for tindakan
const showTindakanModal = ref(false);
const tindakanForm = ref<TindakanItem>({
kodeTindakan: '',
tindakan: '',
tindakanTambahan: ''
});
const editingTindakanIndex = ref<number | null>(null);
const tindakanList = ref(tindakanData);
// Autocomplete options for tindakan
const tindakanOptions = computed(() => {
return tindakanList.value.map(t => ({
title: `${t.kode} - ${t.tindakan}`,
value: t.kode,
tindakan: t.tindakan,
tindakanTambahan: t.tindakan_tambahan
}));
});
// Initialize arrays if not already
if (!Array.isArray(diagnosisItems.value)) {
diagnosisItems.value = [];
}
if (!Array.isArray(tindakanItems.value)) {
tindakanItems.value = [];
}
const openDiagnosisModal = () => {
diagnosisForm.value = {
kodeDiagnosa: '',
diagnosa: '',
jenisDiagnosa: 'utama'
};
editingDiagnosisIndex.value = null;
showDiagnosisModal.value = true;
};
const editDiagnosis = (index: number) => {
diagnosisForm.value = { ...diagnosisItems.value[index] };
editingDiagnosisIndex.value = index;
showDiagnosisModal.value = true;
};
const saveDiagnosis = () => {
if (!diagnosisForm.value.kodeDiagnosa.trim() || !diagnosisForm.value.diagnosa.trim()) return;
if (editingDiagnosisIndex.value !== null) {
diagnosisItems.value[editingDiagnosisIndex.value] = { ...diagnosisForm.value };
} else {
diagnosisItems.value.push({ ...diagnosisForm.value });
}
showDiagnosisModal.value = false;
diagnosisForm.value = {
kodeDiagnosa: '',
diagnosa: '',
jenisDiagnosa: 'utama'
};
editingDiagnosisIndex.value = null;
};
const deleteDiagnosis = (index: number) => {
diagnosisItems.value.splice(index, 1);
};
// Auto-fill diagnosis when code is selected
const handleKodeDiagnosaChange = (kode: string) => {
const found = diagnosisList.value.find(d => d.kode === kode);
if (found) {
diagnosisForm.value.diagnosa = found.diagnosa;
diagnosisForm.value.jenisDiagnosa = found.jenis_diagnosa;
}
};
const openTindakanModal = () => {
tindakanForm.value = {
kodeTindakan: '',
tindakan: '',
tindakanTambahan: ''
};
editingTindakanIndex.value = null;
showTindakanModal.value = true;
};
const editTindakan = (index: number) => {
tindakanForm.value = { ...tindakanItems.value[index] };
editingTindakanIndex.value = index;
showTindakanModal.value = true;
};
const saveTindakan = () => {
if (!tindakanForm.value.kodeTindakan.trim() || !tindakanForm.value.tindakan.trim()) return;
if (editingTindakanIndex.value !== null) {
tindakanItems.value[editingTindakanIndex.value] = { ...tindakanForm.value };
} else {
tindakanItems.value.push({ ...tindakanForm.value });
}
showTindakanModal.value = false;
tindakanForm.value = {
kodeTindakan: '',
tindakan: '',
tindakanTambahan: ''
};
editingTindakanIndex.value = null;
};
const deleteTindakan = (index: number) => {
tindakanItems.value.splice(index, 1);
};
// Auto-fill tindakan when code is selected
const handleKodeTindakanChange = (kode: string) => {
const found = tindakanList.value.find(t => t.kode === kode);
if (found) {
tindakanForm.value.tindakan = found.tindakan;
tindakanForm.value.tindakanTambahan = found.tindakan_tambahan || '';
}
};
</script>
<template>
<v-card elevation="10">
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Medis Pasien</h5>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<!-- Diagnosa -->
<div class="mb-6">
<h6 class="text-h6 font-weight-medium mb-3">Diagnosa</h6>
<v-card variant="outlined" class="mb-3">
<v-alert
v-if="diagnosisItems.length === 0"
type="info"
variant="tonal"
density="compact"
>
<div class="d-flex align-center">
<span>No items</span>
</div>
</v-alert>
<v-list v-else density="compact">
<v-list-item
v-for="(item, index) in diagnosisItems"
:key="index"
class="px-4 mb-2"
@click="editDiagnosis(index)"
style="cursor: pointer;"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<div>
<div class="font-weight-medium">{{ item.kodeDiagnosa }} - {{ item.diagnosa }}</div>
<div class="text-caption text-medium-emphasis">
{{ item.jenisDiagnosa === 'utama' ? 'Diagnosa Utama' : 'Diagnosa Tambahan' }}
</div>
</div>
<v-btn
icon
size="small"
variant="text"
color="error"
@click.stop="deleteDiagnosis(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openDiagnosisModal">
Tambah Diagnosa
</v-btn>
</div>
<!-- Tindakan -->
<div>
<h6 class="text-h6 font-weight-medium mb-3">Tindakan</h6>
<v-card variant="outlined" class="mb-3">
<v-alert
v-if="tindakanItems.length === 0"
type="info"
variant="tonal"
density="compact"
>
<div class="d-flex align-center">
<span>No items</span>
</div>
</v-alert>
<v-list v-else density="compact">
<v-list-item
v-for="(item, index) in tindakanItems"
:key="index"
class="px-4 mb-2"
@click="editTindakan(index)"
style="cursor: pointer;"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<div>
<div class="font-weight-medium">{{ item.kodeTindakan }} - {{ item.tindakan }}</div>
<div v-if="item.tindakanTambahan" class="text-caption text-medium-emphasis">
{{ item.tindakanTambahan }}
</div>
</div>
<v-btn
icon
size="small"
variant="text"
color="error"
@click.stop="deleteTindakan(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openTindakanModal">
Tambah Tindakan
</v-btn>
</div>
</v-card-text>
</v-card>
<!-- Modal for adding diagnosis -->
<v-dialog v-model="showDiagnosisModal" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-package-variant</v-icon>
<span class="text-h6">{{ editingDiagnosisIndex !== null ? 'Edit' : 'Tambah' }} Item Data Diagnosa Pasien Operasi</span>
</div>
<v-btn
icon
variant="text"
@click="showDiagnosisModal = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-6">
<v-row>
<!-- Kode Diagnosa -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Kode Diagnosa</v-label>
<v-autocomplete
v-model="diagnosisForm.kodeDiagnosa"
:items="diagnosisOptions"
placeholder="Kode ICD 10"
variant="outlined"
density="comfortable"
hide-details="auto"
@update:model-value="handleKodeDiagnosaChange"
></v-autocomplete>
</v-col>
<!-- Diagnosa -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Diagnosa</v-label>
<v-text-field
v-model="diagnosisForm.diagnosa"
variant="outlined"
density="comfortable"
readonly
bg-color="grey-lighten-3"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Jenis Diagnosa -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Jenis Diagnosa</v-label>
<v-radio-group v-model="diagnosisForm.jenisDiagnosa" inline hide-details="auto" readonly>
<v-radio label="Diagnosa Utama" value="utama" color="primary"></v-radio>
<v-radio label="Diagnosa Tambahan" value="tambahan" color="primary"></v-radio>
</v-radio-group>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
variant="flat"
@click="saveDiagnosis"
:disabled="!diagnosisForm.kodeDiagnosa.trim() || !diagnosisForm.diagnosa.trim()"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal for adding tindakan -->
<v-dialog v-model="showTindakanModal" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-package-variant</v-icon>
<span class="text-h6">{{ editingTindakanIndex !== null ? 'Edit' : 'Tambah' }} Item Data Tindakan Pasien Operasi</span>
</div>
<v-btn
icon
variant="text"
@click="showTindakanModal = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-6">
<v-row>
<!-- Kode Tindakan -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Kode Tindakan</v-label>
<v-autocomplete
v-model="tindakanForm.kodeTindakan"
:items="tindakanOptions"
placeholder="Kode ICD 9"
variant="outlined"
density="comfortable"
hide-details="auto"
@update:model-value="handleKodeTindakanChange"
></v-autocomplete>
</v-col>
<!-- Tindakan -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Tindakan</v-label>
<v-text-field
v-model="tindakanForm.tindakan"
variant="outlined"
density="comfortable"
readonly
bg-color="grey-lighten-3"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Tindakan Tambahan -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Tindakan Tambahan</v-label>
<v-text-field
v-model="tindakanForm.tindakanTambahan"
placeholder="Prosedur Tindakan Tambahan"
variant="outlined"
density="comfortable"
hide-details="auto"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
variant="flat"
@click="saveTindakan"
:disabled="!tindakanForm.kodeTindakan.trim() || !tindakanForm.tindakan.trim()"
>
<v-icon start>mdi-check</v-icon>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
+365
View File
@@ -0,0 +1,365 @@
<script setup lang="ts">
import dokterData from '~/data/mock/dokter.json';
import spesialisData from '~/data/mock/spesialis.json';
import subSpesialisData from '~/data/mock/subSpesialis.json';
import kategoriOperasiData from '~/data/mock/kategoriOperasi.json';
interface Dokter {
id: number;
nip: string;
nama: string;
satuan_kerja: string;
}
const rencanaOperasiData = defineModel<{
spesialis: number | null | undefined;
subSpesialis: number | null | undefined;
tanggalDaftar: string;
kategoriOperasi: number | null | undefined;
rencanaOperasi: string;
keterangan: string;
}>('rencanaOperasiData', { required: true });
const dokterPelaksanaItems = defineModel<Dokter[]>('dokterPelaksanaItems', { required: true });
const rules = {
required: (value: any) => !!value || 'Field ini wajib diisi'
};
// Spesialis, Sub-spesialis, dan Kategori Operasi data
const spesialisList = ref(spesialisData);
const subSpesialisList = ref(subSpesialisData);
const kategoriOperasiList = ref(kategoriOperasiData);
// Autocomplete options
const spesialisOptions = computed(() => {
return spesialisList.value.map(s => ({
title: s.spesialis,
value: s.id
}));
});
const subSpesialisOptions = computed(() => {
if (!rencanaOperasiData.value.spesialis) return [];
return subSpesialisList.value
.filter(ss => ss.id_spesialis === rencanaOperasiData.value.spesialis)
.map(ss => ({
title: ss.subspesialis,
value: ss.id
}));
});
const kategoriOperasiOptions = computed(() => {
return kategoriOperasiList.value.map(k => ({
title: k.kategori,
value: k.id
}));
});
// Watch spesialis change to reset sub-spesialis
watch(() => rencanaOperasiData.value.spesialis, () => {
rencanaOperasiData.value.subSpesialis = null;
});
// Modal state for dokter
const showDokterModal = ref(false);
const searchDokter = ref('');
const selectedDokters = ref<number[]>([]);
const dokterList = ref<Dokter[]>(dokterData);
// Computed filtered dokter list
const filteredDokterList = computed(() => {
if (!searchDokter.value) return dokterList.value;
const search = searchDokter.value.toLowerCase();
return dokterList.value.filter(dokter =>
dokter.nama.toLowerCase().includes(search) ||
dokter.nip.includes(search) ||
dokter.satuan_kerja.toLowerCase().includes(search)
);
});
// Initialize dokterPelaksanaItems as array if not already
if (!Array.isArray(dokterPelaksanaItems.value)) {
dokterPelaksanaItems.value = [];
}
const openDokterModal = () => {
// Pre-select already added doctors
selectedDokters.value = dokterPelaksanaItems.value.map(d => d.id);
searchDokter.value = '';
showDokterModal.value = true;
};
const saveDokterSelection = () => {
// Get selected doctor objects
const selected = dokterList.value.filter(d => selectedDokters.value.includes(d.id));
dokterPelaksanaItems.value = selected;
showDokterModal.value = false;
};
const deleteDokter = (index: number) => {
dokterPelaksanaItems.value.splice(index, 1);
};
// Toggle all selection
const toggleAllDokters = () => {
if (selectedDokters.value.length === filteredDokterList.value.length) {
selectedDokters.value = [];
} else {
selectedDokters.value = filteredDokterList.value.map(d => d.id);
}
};
const isAllSelected = computed(() => {
return filteredDokterList.value.length > 0 &&
selectedDokters.value.length === filteredDokterList.value.length;
});
const toggleDokterSelection = (dokterId: number) => {
const index = selectedDokters.value.indexOf(dokterId);
if (index > -1) {
selectedDokters.value.splice(index, 1);
} else {
selectedDokters.value.push(dokterId);
}
};
</script>
<template>
<v-card elevation="10">
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Rencana Operasi</h5>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-row>
<!-- Spesialis -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Spesialis <span class="text-error">*</span></v-label>
<v-autocomplete
v-model="rencanaOperasiData.spesialis"
:items="spesialisOptions"
placeholder="Select an item..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
></v-autocomplete>
</v-col>
<!-- Sub Spesialis -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Sub Spesialis <span class="text-error">*</span></v-label>
<v-autocomplete
v-model="rencanaOperasiData.subSpesialis"
:items="subSpesialisOptions"
placeholder="Select an item..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:disabled="!rencanaOperasiData.spesialis"
></v-autocomplete>
</v-col>
<!-- Dokter Pelaksana -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Dokter Pelaksana</v-label>
<v-card variant="outlined" class="mb-3">
<v-alert
v-if="dokterPelaksanaItems.length === 0"
type="info"
variant="tonal"
density="compact"
>
<div class="d-flex align-center">
<span>No items</span>
</div>
</v-alert>
<v-list v-else density="compact">
<v-list-item
v-for="(item, index) in dokterPelaksanaItems"
:key="index"
@click="openDokterModal"
class="px-4 mb-2"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<div>
<div class="font-weight-medium">{{ item.nama }}</div>
<div class="text-caption text-medium-emphasis">
{{ item.nip }} - {{ item.satuan_kerja }}
</div>
</div>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="deleteDokter(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openDokterModal">
Tambah Dokter Pelaksana
</v-btn>
</v-col>
<!-- Tanggal Daftar -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Tanggal Daftar <span class="text-error">*</span></v-label>
<v-text-field
v-model="rencanaOperasiData.tanggalDaftar"
type="datetime-local"
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Kategori Operasi -->
<v-col cols="12" md="6">
<v-label class="mb-2 font-weight-medium">Kategori Operasi <span class="text-error">*</span></v-label>
<v-autocomplete
v-model="rencanaOperasiData.kategoriOperasi"
:items="kategoriOperasiOptions"
placeholder="Select an item..."
variant="outlined"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
></v-autocomplete>
</v-col>
<!-- Rencana Operasi -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Rencana Operasi</v-label>
<v-textarea
v-model="rencanaOperasiData.rencanaOperasi"
variant="outlined"
rows="5"
hide-details="auto"
></v-textarea>
</v-col>
<!-- Keterangan -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Keterangan</v-label>
<v-textarea
v-model="rencanaOperasiData.keterangan"
variant="outlined"
rows="3"
hide-details="auto"
></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Modal for selecting dokter -->
<v-dialog v-model="showDokterModal" max-width="900px">
<v-card>
<v-card-title class="d-flex align-center justify-space-between pa-4">
<div class="d-flex align-center">
<v-icon class="mr-3">mdi-database</v-icon>
<span class="text-h6">Select Item</span>
</div>
<v-btn
icon
variant="text"
@click="showDokterModal = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-4">
<!-- Search Bar -->
<v-text-field
v-model="searchDokter"
placeholder="Search..."
variant="outlined"
density="comfortable"
hide-details
prepend-inner-icon="mdi-magnify"
class="mb-4"
></v-text-field>
<!-- Info Badge -->
<div class="text-caption text-medium-emphasis mb-3">
1-{{ filteredDokterList.length }} of {{ dokterList.length }} Items
</div>
<!-- Data Table -->
<v-table density="compact" class="border">
<thead>
<tr>
<th class="text-left">
<v-checkbox
:model-value="isAllSelected"
@click="toggleAllDokters"
hide-details
density="compact"
></v-checkbox>
</th>
<th class="text-left">Display</th>
<th class="text-left">Nip</th>
<th class="text-left">Nama Lengkap</th>
</tr>
</thead>
<tbody>
<tr
v-for="dokter in filteredDokterList"
:key="dokter.id"
@click="toggleDokterSelection(dokter.id)"
class="cursor-pointer"
style="cursor: pointer;"
>
<td @click.stop>
<v-checkbox
v-model="selectedDokters"
:value="dokter.id"
hide-details
density="compact"
></v-checkbox>
</td>
<td>Doctor</td>
<td>{{ dokter.nip }}</td>
<td>{{ dokter.nama }}</td>
</tr>
</tbody>
</v-table>
<!-- Empty State -->
<div v-if="filteredDokterList.length === 0" class="text-center py-8">
<v-icon size="large" class="mb-2 text-medium-emphasis">mdi-information-outline</v-icon>
<p class="text-medium-emphasis">No results found</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4 justify-end">
<v-btn
color="primary"
variant="flat"
@click="saveDokterSelection"
:disabled="selectedDokters.length === 0"
>
<v-icon start>mdi-check</v-icon>
Save ({{ selectedDokters.length }})
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
const statusPasienData = defineModel<{
tanggalSelesai: string;
statusOperasi: string;
keteranganStatus: string;
}>({ required: true });
</script>
<template>
<v-card elevation="10">
<v-card-item>
<h5 class="text-h5 font-weight-bold">Data Status Pasien Operasi</h5>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-row>
<!-- Status Operasi -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Status Operasi <span class="text-error">*</span></v-label>
<v-row class="mt-2">
<v-col cols="12" sm="6" md="3">
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Belum' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Belum' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Belum'"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Belum' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Belum
</v-btn>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Selesai' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Selesai' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Selesai'"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Selesai' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Selesai
</v-btn>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Tunda' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Tunda' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Tunda'"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Tunda' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Tunda
</v-btn>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Batal' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Batal' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Batal'"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Batal' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Batal
</v-btn>
</v-col>
</v-row>
</v-col>
<!-- Tanggal Selesai Operasi -->
<v-col cols="6">
<v-label class="mb-2 font-weight-medium">Tanggal Selesai Operasi</v-label>
<v-text-field
v-model="statusPasienData.tanggalSelesai"
type="date"
variant="outlined"
density="comfortable"
hide-details="auto"
></v-text-field>
</v-col>
<!-- Keterangan Status Pasien -->
<v-col cols="12">
<v-label class="mb-2 font-weight-medium">Keterangan Status Pasien</v-label>
<v-textarea
v-model="statusPasienData.keteranganStatus"
variant="outlined"
rows="5"
placeholder="Keterangan status tunda / batal"
hide-details="auto"
></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
+8 -16
View File
@@ -11,27 +11,19 @@ const props = defineProps({
</script>
<template>
<v-card elevation="10" class="mb-8">
<div class="px-6 py-4">
<div class="d-flex justify-space-between align-center">
<h5 class="text-h5">{{ title }}</h5>
<v-breadcrumbs :items="breadcrumbs" class="pa-0">
<template v-slot:prepend>
<router-link to="/" class="textSecondary lh-0">
<Icon icon="solar:home-2-line-duotone" height="20" />
</router-link>
</template>
<template v-slot:divider>
<div class="d-flex align-center textSecondary "></div>
</template>
<template v-slot:title="{ item }">
<v-chip size="small" class="rounded-sm" color="primary" >{{ item.text }}</v-chip>
</template>
</v-breadcrumbs>
<div class="d-flex align-center ga-2">
<router-link to="/" class="textSecondary lh-0">
<Icon icon="solar:home-2-line-duotone" height="20" />
</router-link>
<v-chip size="small" class="rounded-sm" color="primary">
{{ breadcrumbs?.map(item => item.text).join(' / ') }}
</v-chip>
</div>
</div>
</div>
</v-card>
</template>
<style lang="scss">