fix(FE) : adjust antrian table, feat antrian kategori
This commit is contained in:
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user