fix(FE) : adjust antrian table, feat antrian kategori

This commit is contained in:
Yusron alamsyah
2026-01-26 14:51:32 +07:00
parent cbd5dcf8f5
commit afdb94e942
14 changed files with 451 additions and 93 deletions

No files matched your search

@@ -37,12 +37,33 @@ const sidebarItem: menu[] = [
icon: 'cart-3-line-duotone',
to: '/antrean/all',
},
{
title: 'Kategori',
icon: 'layers-line-duotone',
to: '/antrean/list-kategori'
},
{
title: 'Spesialis',
icon: 'widget-4-line-duotone',
to: '/antrean/list-spesialis'
}
]
},
{
header: 'pengaturan',
id: 1,
children: [
{
title: 'Hak Akses',
icon: 'settings-line-duotone',
to: '/setting/hak-akses',
},
{
title: 'User',
icon: 'user-line-duotone',
to: '/setting/user'
}
]
}
];
+49 -12
View File
@@ -14,10 +14,25 @@ const rules = {
required: (value: string) => !!value || 'Field ini wajib diisi'
};
// Phone validation rules
const phoneLength = (value: string) => {
if (!value) return true;
// Hapus karakter non-angka sebelum menghitung panjang
const length = value.replace(/[^0-9]/g, '').length;
return (length >= 9 && length <= 15) || 'Nomor telepon harus antara 9 hingga 15 digit.';
};
const indonesianPhoneFormat = (value: string) => {
if (!value) return true;
// Pola: Harus dimulai dengan '08', diikuti 7 hingga 13 digit angka
return /^08[0-9]{7,13}$/.test(value) || 'Format nomor telepon tidak valid (Contoh: 08xxxxxxxx).';
};
// Modal state
const showModal = ref(false);
const newPhoneNumber = ref('');
const editingIndex = ref<number | null>(null);
const phoneError = ref<string | boolean>(true);
// Initialize nomorTelepon as array if not already
if (!Array.isArray(formData.value.nomorTelepon)) {
@@ -27,18 +42,19 @@ if (!Array.isArray(formData.value.nomorTelepon)) {
const openAddModal = () => {
newPhoneNumber.value = '';
editingIndex.value = null;
phoneError.value = true;
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;
newPhoneNumber.value = phone;
editingIndex.value = index;
phoneError.value = true;
showModal.value = true;
};
// Handle only numeric input
// Handle only numeric input and validate
const handlePhoneInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
@@ -46,22 +62,42 @@ const handlePhoneInput = (event: Event) => {
// Only allow numbers
const numericValue = value.replace(/\D/g, '');
newPhoneNumber.value = numericValue;
// Validate format and length
const lengthValidation = phoneLength(numericValue);
const formatValidation = indonesianPhoneFormat(numericValue);
if (lengthValidation !== true) {
phoneError.value = lengthValidation;
} else if (formatValidation !== true) {
phoneError.value = formatValidation;
} else {
phoneError.value = true;
}
};
const savePhoneNumber = () => {
if (!newPhoneNumber.value.trim()) return;
const fullPhoneNumber = `+62${newPhoneNumber.value}`;
// Validate before saving
const lengthValidation = phoneLength(newPhoneNumber.value);
const formatValidation = indonesianPhoneFormat(newPhoneNumber.value);
if (lengthValidation !== true || formatValidation !== true) {
return;
}
// Save directly as 08xxx format
if (editingIndex.value !== null) {
formData.value.nomorTelepon[editingIndex.value] = fullPhoneNumber;
formData.value.nomorTelepon[editingIndex.value] = newPhoneNumber.value;
} else {
formData.value.nomorTelepon.push(fullPhoneNumber);
formData.value.nomorTelepon.push(newPhoneNumber.value);
}
showModal.value = false;
newPhoneNumber.value = '';
editingIndex.value = null;
phoneError.value = true;
};
const deletePhoneNumber = (index: number) => {
@@ -85,6 +121,7 @@ const deletePhoneNumber = (index: number) => {
v-model="formData.noRekamMedis"
placeholder="Masukan Nomor RM / Nama Pasien"
variant="outlined"
maxlength="8"
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
@@ -217,17 +254,17 @@ const deletePhoneNumber = (index: number) => {
<v-label class="mb-2 font-weight-medium">Nomor Telepon</v-label>
<v-text-field
v-model="newPhoneNumber"
placeholder="Masukan nomor telepon lengkap"
placeholder="Contoh: 08123456789"
variant="outlined"
density="comfortable"
hide-details="auto"
type="tel"
@input="handlePhoneInput"
:error-messages="phoneError !== true ? [phoneError as string] : []"
>
<template #prepend-inner>
<span class="text-medium-emphasis">+62</span>
</template>
</v-text-field>
<div class="text-caption text-medium-emphasis mt-2">
Format: 08xxxxxxxxx (9-15 digit)
</div>
</v-card-text>
<v-divider></v-divider>
@@ -237,7 +274,7 @@ const deletePhoneNumber = (index: number) => {
color="primary"
variant="flat"
@click="savePhoneNumber"
:disabled="!newPhoneNumber.trim()"
:disabled="!newPhoneNumber.trim() || phoneError !== true"
>
<v-icon start>mdi-check</v-icon>
Save
+80
View File
@@ -0,0 +1,80 @@
<script setup lang="ts">
interface Header {
title: string;
key: string;
sortable?: boolean;
width?: string;
align?: 'start' | 'center' | 'end';
}
interface Props {
headers: Header[];
items: any[];
search?: string;
itemsPerPage?: number;
minWidth?: string;
}
const props = withDefaults(defineProps<Props>(), {
search: '',
itemsPerPage: 10,
minWidth: '1200px'
});
const currentPage = ref(1);
const itemsPerPageLocal = ref(props.itemsPerPage);
</script>
<template>
<div class="table-wrapper">
<v-data-table
:headers="headers"
:items="items"
:search="search"
class="elevation-1"
item-value="id"
v-model:page="currentPage"
v-model:items-per-page="itemsPerPageLocal"
>
<!-- Pass through all slots to parent -->
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
<!-- Default slot for Jenis Kelamin if not provided -->
<template #item.jenisKelamin="{ item }">
<slot name="item.jenisKelamin" :item="item">
<v-chip
:color="item.jenisKelamin === 'L' ? 'primary' : 'pink'"
size="small"
>
{{ item.jenisKelamin }}
</v-chip>
</slot>
</template>
<!-- Default slot for Status if not provided -->
<template #item.status="{ item }">
<slot name="item.status" :item="item">
<v-chip
:color="item.status === 'selesai' ? 'success' : item.status === 'batal' ? 'error' : item.status === 'tunda' ? 'warning' : 'info'"
size="small"
>
{{ item.status }}
</v-chip>
</slot>
</template>
</v-data-table>
</div>
</template>
<style scoped>
.table-wrapper {
overflow-x: auto;
width: 100%;
}
.table-wrapper :deep(.v-data-table) {
min-width: v-bind(minWidth);
}
</style>
+19 -2
View File
@@ -1,19 +1,36 @@
<script setup >
import { Icon } from '@iconify/vue';
const props = defineProps({
title: String,
breadcrumbs: Array ,
icon: String,
text: String
text: String,
buttonBack: Boolean
});
const router = useRouter();
const handleBack = () => {
router.back();
};
</script>
<template>
<div class="px-6 py-4">
<div class="d-flex justify-space-between align-center">
<h5 class="text-h5">{{ title }}</h5>
<div class="d-flex align-center ga-2">
<v-btn
v-if="buttonBack"
icon
variant="text"
size="small"
@click="handleBack"
>
<Icon icon="solar:arrow-left-line-duotone" height="24" />
</v-btn>
<h5 class="text-h5">{{ title }}</h5>
</div>
<div class="d-flex align-center ga-2">
<router-link to="/" class="textSecondary lh-0">
<Icon icon="solar:home-2-line-duotone" height="20" />