fix(FE) : validate number norm & add action, feat user & hak akses

This commit is contained in:
Yusron alamsyah
2026-01-27 10:58:23 +07:00
parent afdb94e942
commit d826b43887
13 changed files with 1283 additions and 34 deletions
+29 -5
View File
@@ -1,4 +1,12 @@
<script setup lang="ts">
interface Props {
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
});
const formData = defineModel<{
noRekamMedis: string;
noKtp: string;
@@ -103,6 +111,16 @@ const savePhoneNumber = () => {
const deletePhoneNumber = (index: number) => {
formData.value.nomorTelepon.splice(index, 1);
};
// Handle noRekamMedis input - only allow numbers
const handleRmInput = (event: Event) => {
const input = event.target as HTMLInputElement;
const value = input.value;
// Only allow numbers
const numericValue = value.replace(/\D/g, '');
formData.value.noRekamMedis = numericValue;
};
</script>
<template>
@@ -122,9 +140,13 @@ const deletePhoneNumber = (index: number) => {
placeholder="Masukan Nomor RM / Nama Pasien"
variant="outlined"
maxlength="8"
density="comfortable"
density="comfortable"
type="tel"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
@input="handleRmInput"
></v-text-field>
</v-col>
@@ -145,7 +167,7 @@ const deletePhoneNumber = (index: number) => {
<!-- 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-group v-model="formData.jenisKelamin" inline hide-details="auto" :readonly="readonly" :disabled="readonly">
<v-radio label="Laki-Laki" value="L" color="primary"></v-radio>
<v-radio label="Perempuan" value="P" color="primary"></v-radio>
</v-radio-group>
@@ -197,13 +219,14 @@ const deletePhoneNumber = (index: number) => {
:key="index"
class="px-4 mb-2"
border
@click="editPhoneNumber(index)"
style="cursor: pointer;"
@click="!readonly && editPhoneNumber(index)"
:style="readonly ? 'cursor: default;' : 'cursor: pointer;'"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
<span>{{ phone }}</span>
<v-btn
v-if="!readonly"
icon
size="small"
variant="text"
@@ -219,6 +242,7 @@ const deletePhoneNumber = (index: number) => {
<!-- Add Button -->
<v-btn
v-if="!readonly"
color="primary"
variant="flat"
prepend-icon="mdi-plus"
@@ -277,7 +301,7 @@ const deletePhoneNumber = (index: number) => {
:disabled="!newPhoneNumber.trim() || phoneError !== true"
>
<v-icon start>mdi-check</v-icon>
Save
Simpan
</v-btn>
</v-card-actions>
</v-card>
+18 -8
View File
@@ -14,6 +14,14 @@ interface TindakanItem {
tindakanTambahan?: string;
}
interface Props {
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
});
const diagnosisItems = defineModel<DiagnosisItem[]>('diagnosisItems', { required: true });
const tindakanItems = defineModel<TindakanItem[]>('tindakanItems', { required: true });
@@ -186,8 +194,8 @@ const handleKodeTindakanChange = (kode: string) => {
v-for="(item, index) in diagnosisItems"
:key="index"
class="px-4 mb-2"
@click="editDiagnosis(index)"
style="cursor: pointer;"
@click="!readonly && editDiagnosis(index)"
:style="readonly ? 'cursor: default;' : 'cursor: pointer;'"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
@@ -198,6 +206,7 @@ const handleKodeTindakanChange = (kode: string) => {
</div>
</div>
<v-btn
v-if="!readonly"
icon
size="small"
variant="text"
@@ -211,7 +220,7 @@ const handleKodeTindakanChange = (kode: string) => {
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openDiagnosisModal">
<v-btn v-if="!readonly" color="primary" prepend-icon="mdi-plus" @click="openDiagnosisModal">
Tambah Diagnosa
</v-btn>
</div>
@@ -235,8 +244,8 @@ const handleKodeTindakanChange = (kode: string) => {
v-for="(item, index) in tindakanItems"
:key="index"
class="px-4 mb-2"
@click="editTindakan(index)"
style="cursor: pointer;"
@click="!readonly && editTindakan(index)"
:style="readonly ? 'cursor: default;' : 'cursor: pointer;'"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
@@ -247,6 +256,7 @@ const handleKodeTindakanChange = (kode: string) => {
</div>
</div>
<v-btn
v-if="!readonly"
icon
size="small"
variant="text"
@@ -260,7 +270,7 @@ const handleKodeTindakanChange = (kode: string) => {
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openTindakanModal">
<v-btn v-if="!readonly" color="primary" prepend-icon="mdi-plus" @click="openTindakanModal">
Tambah Tindakan
</v-btn>
</div>
@@ -336,7 +346,7 @@ const handleKodeTindakanChange = (kode: string) => {
:disabled="!diagnosisForm.kodeDiagnosa.trim() || !diagnosisForm.diagnosa.trim()"
>
<v-icon start>mdi-check</v-icon>
Save
Simpan
</v-btn>
</v-card-actions>
</v-card>
@@ -414,7 +424,7 @@ const handleKodeTindakanChange = (kode: string) => {
:disabled="!tindakanForm.kodeTindakan.trim() || !tindakanForm.tindakan.trim()"
>
<v-icon start>mdi-check</v-icon>
Save
Simpan
</v-btn>
</v-card-actions>
</v-card>
+28 -4
View File
@@ -11,6 +11,14 @@ interface Dokter {
satuan_kerja: string;
}
interface Props {
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
});
const rencanaOperasiData = defineModel<{
spesialis: number | null | undefined;
subSpesialis: number | null | undefined;
@@ -146,6 +154,9 @@ const toggleDokterSelection = (dokterId: number) => {
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:disabled="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
@@ -160,7 +171,9 @@ const toggleDokterSelection = (dokterId: number) => {
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:disabled="!rencanaOperasiData.spesialis"
:disabled="readonly || !rencanaOperasiData.spesialis"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
@@ -182,8 +195,9 @@ const toggleDokterSelection = (dokterId: number) => {
<v-list-item
v-for="(item, index) in dokterPelaksanaItems"
:key="index"
@click="openDokterModal"
@click="!readonly && openDokterModal()"
class="px-4 mb-2"
:style="readonly ? 'cursor: default;' : 'cursor: pointer;'"
>
<template #default>
<div class="d-flex align-center justify-space-between w-100">
@@ -194,6 +208,7 @@ const toggleDokterSelection = (dokterId: number) => {
</div>
</div>
<v-btn
v-if="!readonly"
icon
size="small"
variant="text"
@@ -207,7 +222,7 @@ const toggleDokterSelection = (dokterId: number) => {
</v-list-item>
</v-list>
</v-card>
<v-btn color="primary" prepend-icon="mdi-plus" @click="openDokterModal">
<v-btn v-if="!readonly" color="primary" prepend-icon="mdi-plus" @click="openDokterModal">
Tambah Dokter Pelaksana
</v-btn>
</v-col>
@@ -222,6 +237,8 @@ const toggleDokterSelection = (dokterId: number) => {
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-text-field>
</v-col>
@@ -236,6 +253,9 @@ const toggleDokterSelection = (dokterId: number) => {
density="comfortable"
:rules="[rules.required]"
hide-details="auto"
:readonly="readonly"
:disabled="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-autocomplete>
</v-col>
@@ -247,6 +267,8 @@ const toggleDokterSelection = (dokterId: number) => {
variant="outlined"
rows="5"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-textarea>
</v-col>
@@ -258,6 +280,8 @@ const toggleDokterSelection = (dokterId: number) => {
variant="outlined"
rows="3"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-textarea>
</v-col>
</v-row>
@@ -357,7 +381,7 @@ const toggleDokterSelection = (dokterId: number) => {
:disabled="selectedDokters.length === 0"
>
<v-icon start>mdi-check</v-icon>
Save ({{ selectedDokters.length }})
Simpan ({{ selectedDokters.length }})
</v-btn>
</v-card-actions>
</v-card>
+22 -6
View File
@@ -1,4 +1,12 @@
<script setup lang="ts">
interface Props {
readonly?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
readonly: false
});
const statusPasienData = defineModel<{
tanggalSelesai: string;
statusOperasi: string;
@@ -24,7 +32,8 @@ const statusPasienData = defineModel<{
size="large"
:variant="statusPasienData.statusOperasi === 'Belum' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Belum' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Belum'"
@click="!readonly && (statusPasienData.statusOperasi = 'Belum')"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Belum' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Belum
@@ -36,7 +45,8 @@ const statusPasienData = defineModel<{
size="large"
:variant="statusPasienData.statusOperasi === 'Selesai' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Selesai' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Selesai'"
@click="!readonly && (statusPasienData.statusOperasi = 'Selesai')"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Selesai' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Selesai
@@ -48,7 +58,8 @@ const statusPasienData = defineModel<{
size="large"
:variant="statusPasienData.statusOperasi === 'Tunda' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Tunda' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Tunda'"
@click="!readonly && (statusPasienData.statusOperasi = 'Tunda')"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Tunda' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Tunda
@@ -60,7 +71,8 @@ const statusPasienData = defineModel<{
size="large"
:variant="statusPasienData.statusOperasi === 'Batal' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Batal' ? 'primary' : 'default'"
@click="statusPasienData.statusOperasi = 'Batal'"
@click="!readonly && (statusPasienData.statusOperasi = 'Batal')"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Batal' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Batal
@@ -78,6 +90,8 @@ const statusPasienData = defineModel<{
variant="outlined"
density="comfortable"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
></v-text-field>
</v-col>
@@ -87,9 +101,11 @@ const statusPasienData = defineModel<{
<v-textarea
v-model="statusPasienData.keteranganStatus"
variant="outlined"
rows="5"
placeholder="Keterangan status tunda / batal"
rows="3"
hide-details="auto"
:readonly="readonly"
:bg-color="readonly ? 'grey-lighten-3' : undefined"
placeholder="Keterangan status tunda / batal"
></v-textarea>
</v-col>
</v-row>
+40 -1
View File
@@ -7,20 +7,37 @@ interface Header {
align?: 'start' | 'center' | 'end';
}
interface Action {
icon: string;
color?: string;
tooltip?: string;
event: string;
}
interface Props {
headers: Header[];
items: any[];
search?: string;
itemsPerPage?: number;
minWidth?: string;
actions?: Action[];
}
const props = withDefaults(defineProps<Props>(), {
search: '',
itemsPerPage: 10,
minWidth: '1200px'
minWidth: '1200px',
actions: () => []
});
const emit = defineEmits<{
(e: string, item: any): void;
}>();
const handleAction = (event: string, item: any) => {
emit(event, item);
};
const currentPage = ref(1);
const itemsPerPageLocal = ref(props.itemsPerPage);
</script>
@@ -64,6 +81,28 @@ const itemsPerPageLocal = ref(props.itemsPerPage);
</v-chip>
</slot>
</template>
<!-- Action slot if enabled -->
<template v-if="actions && actions.length > 0" #item.actions="{ item }">
<slot name="item.actions" :item="item">
<div class="d-flex ga-2">
<v-btn
v-for="(action, index) in actions"
:key="index"
icon
size="small"
variant="text"
:color="action.color || 'primary'"
@click="handleAction(action.event, item)"
>
<v-icon>{{ action.icon }}</v-icon>
<v-tooltip v-if="action.tooltip" activator="parent" location="top">
{{ action.tooltip }}
</v-tooltip>
</v-btn>
</div>
</slot>
</template>
</v-data-table>
</div>
</template>