Merge branch 'dev' into feat/encounter
This commit is contained in:
No files matched your search
@@ -174,7 +174,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, table, label {
|
body, table, label {
|
||||||
@apply md:!text-xs 2xl:!text-sm;
|
@apply md:!text-xs 2xl:!text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||||
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||||
|
import Button from '~/components/pub/ui/button/Button.vue'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { DiagnoseSrcFormData } from '~/schemas/diagnose-src.schema'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import type z from 'zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
schema: z.ZodSchema<any>
|
||||||
|
values: any
|
||||||
|
isLoading?: boolean
|
||||||
|
isReadonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isLoading = props.isLoading !== undefined ? props.isLoading : false
|
||||||
|
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: DiagnoseSrcFormData, resetForm: () => void]
|
||||||
|
cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defineField, errors, meta } = useForm({
|
||||||
|
validationSchema: toTypedSchema(props.schema),
|
||||||
|
initialValues: {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
parent_id: null,
|
||||||
|
} as Partial<DiagnoseSrcFormData>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [code, codeAttrs] = defineField('code')
|
||||||
|
const [name, nameAttrs] = defineField('name')
|
||||||
|
const [indName, indNameAttrs] = defineField('indName')
|
||||||
|
|
||||||
|
if (props.values) {
|
||||||
|
if (props.values.code !== undefined) code.value = props.values.code
|
||||||
|
if (props.values.name !== undefined) name.value = props.values.name
|
||||||
|
if (props.values.indName !== undefined) indName.value = props.values.indName
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
code.value = ''
|
||||||
|
name.value = ''
|
||||||
|
indName.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitForm() {
|
||||||
|
const formData: DiagnoseSrcFormData = {
|
||||||
|
code: code.value || '',
|
||||||
|
name: name.value || '',
|
||||||
|
indName: indName.value || null,
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelForm() {
|
||||||
|
emit('cancel', resetForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
id="form-diagnose-src"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<Block
|
||||||
|
labelSize="thin"
|
||||||
|
class="!mb-2.5 !pt-0 xl:!mb-3"
|
||||||
|
:colCount="1"
|
||||||
|
>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Kode</Label>
|
||||||
|
<Field :errMessage="errors.code">
|
||||||
|
<Input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
v-bind="codeAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama (FHIR)</Label>
|
||||||
|
<Field :errMessage="errors.name">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="name"
|
||||||
|
v-bind="nameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama (ID)</Label>
|
||||||
|
<Field :errMessage="errors.indName">
|
||||||
|
<Input
|
||||||
|
id="indName"
|
||||||
|
v-model="indName"
|
||||||
|
v-bind="indNameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<div class="my-2 flex justify-end gap-2 py-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-[120px]"
|
||||||
|
@click="onCancelForm"
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!isReadonly"
|
||||||
|
type="button"
|
||||||
|
class="w-[120px]"
|
||||||
|
:disabled="isLoading || !meta.valid"
|
||||||
|
@click="onSubmitForm"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, { width: 50 }],
|
||||||
|
|
||||||
|
headers: [[{ label: 'Kode' }, { label: 'Nama (FHIR)' }, { label: 'Nama (ID)' }, { label: '' }]],
|
||||||
|
|
||||||
|
keys: ['code', 'name', 'indName', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama (FHIR)' },
|
||||||
|
{ key: 'indName', label: 'Nama (ID)' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
|
// Configs
|
||||||
|
import { config } from './list-cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -108,248 +108,246 @@ function onAddSep() {
|
|||||||
:initial-values="initialValues"
|
:initial-values="initialValues"
|
||||||
>
|
>
|
||||||
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
|
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
|
||||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
<div class="flex flex-col justify-between">
|
||||||
<div class="flex flex-col justify-between">
|
<div class="mb-2 2xl:mb-3 text-sm 2xl:text-base font-semibold">
|
||||||
<div class="p-2">
|
Data Pasien
|
||||||
<h2 class="text-md font-semibold">Data Pasien</h2>
|
|
||||||
</div>
|
|
||||||
<div class="my-2 flex gap-6 p-2 text-sm">
|
|
||||||
<span>
|
|
||||||
Sudah pernah terdaftar sebagai pasien?
|
|
||||||
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'search')">
|
|
||||||
<Icon name="i-lucide-search" class="mr-1" /> Cari Pasien
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
Belum pernah terdaftar sebagai pasien?
|
|
||||||
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'add')">
|
|
||||||
<Icon name="i-lucide-plus" class="mr-1" /> Tambah Pasien Baru
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Block :colCount="3">
|
|
||||||
<Cell>
|
|
||||||
<Label label-for="patient_name">Nama Pasien</Label>
|
|
||||||
<Field id="patient_name" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="patient_name">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
id="patient_name"
|
|
||||||
v-bind="componentField"
|
|
||||||
disabled
|
|
||||||
placeholder="Tambah data pasien terlebih dahulu"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- NIK -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="nik">NIK</Label>
|
|
||||||
<Field id="nik" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="nik">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input id="nik" v-bind="componentField" disabled placeholder="Otomatis" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
<Cell>
|
|
||||||
<Label label-for="rm">No. RM</Label>
|
|
||||||
<Field id="rm" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="rm">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input id="rm" v-bind="componentField" disabled placeholder="RM99222" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
</Block>
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div class="p-2">
|
|
||||||
<h2 class="text-md font-semibold">Data Kunjungan</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Block :colCount="3">
|
|
||||||
<!-- Dokter (Combobox) -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="doctor_id">Dokter</Label>
|
|
||||||
<Field id="doctor_id" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="doctor_id">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Combobox id="doctor_id" v-bind="componentField" :items="doctorOpts" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- Tanggal Daftar (DatePicker) -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="register_date">Tanggal Daftar</Label>
|
|
||||||
<Field id="register_date" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="register_date">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<DatepickerSingle v-bind="componentField" placeholder="Pilih tanggal" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- Jenis Pembayaran (Combobox) -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="payment_type">Jenis Pembayaran</Label>
|
|
||||||
<Field id="payment_type" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="payment_type">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<!-- <Combobox id="payment_type" v-bind="componentField" :items="paymentOpts" /> -->
|
|
||||||
<Select id="payment_type" v-bind="componentField" :items="paymentOpts" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block :colCount="3">
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="bpjs_number">Kelompok Peserta</Label>
|
|
||||||
<Field id="bpjs_number" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
id="bpjs_number"
|
|
||||||
v-bind="componentField"
|
|
||||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- No. Kartu BPJS -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="bpjs_number">No. Kartu BPJS</Label>
|
|
||||||
<Field id="bpjs_number" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="bpjs_number">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
id="bpjs_number"
|
|
||||||
v-bind="componentField"
|
|
||||||
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- Jenis SEP -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="sep_type">Jenis SEP</Label>
|
|
||||||
<Field id="sep_type" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="sep_type">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Select id="sep_type" v-bind="componentField" :items="sepOpts" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block :colCount="3">
|
|
||||||
<!-- No. SEP (input + tombol +) -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="sep_number">No. SEP</Label>
|
|
||||||
<Field id="sep_number" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="sep_number">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="sep_number"
|
|
||||||
v-bind="componentField"
|
|
||||||
placeholder="Tambah SEP terlebih dahulu"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<Button class="bg-primary" size="sm" variant="outline" @click.prevent="onAddSep">+</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- Dokumen SEP (file) -->
|
|
||||||
<Cell :cosSpan="3">
|
|
||||||
<Label label-for="sep_file">Dokumen SEP</Label>
|
|
||||||
<Field id="sep_file" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="sep_file">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input ref="sepFileInput" type="file" class="hidden" @change="onSepFileChange" />
|
|
||||||
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSepFile"
|
|
||||||
>Pilih Berkas</Button
|
|
||||||
>
|
|
||||||
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SEP" />
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
|
|
||||||
<!-- Dokumen SIPP (file) -->
|
|
||||||
<Cell :cosSpan="3" labelSize="thin">
|
|
||||||
<Label label-for="sipp_file">Dokumen SIPP</Label>
|
|
||||||
<Field id="sipp_file" :errors="errors">
|
|
||||||
<FormField v-slot="{ componentField }" name="sipp_file">
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input ref="sippFileInput" type="file" class="hidden" @change="onSippFileChange" />
|
|
||||||
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSippFile"
|
|
||||||
>Pilih Berkas</Button
|
|
||||||
>
|
|
||||||
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SIPP" />
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</Cell>
|
|
||||||
</Block>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-6 mb-2 2xl:mb-2">
|
||||||
|
<span>
|
||||||
|
Sudah pernah terdaftar sebagai pasien?
|
||||||
|
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'search')">
|
||||||
|
<Icon name="i-lucide-search" class="mr-1" /> Cari Pasien
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Belum pernah terdaftar sebagai pasien?
|
||||||
|
<Button class="bg-primary" size="sm" @click.prevent="emit('click', 'add')">
|
||||||
|
<Icon name="i-lucide-plus" class="mr-1" /> Tambah Pasien Baru
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Block :colCount="3">
|
||||||
|
<Cell>
|
||||||
|
<Label label-for="patient_name">Nama Pasien</Label>
|
||||||
|
<Field id="patient_name" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="patient_name">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="patient_name"
|
||||||
|
v-bind="componentField"
|
||||||
|
disabled
|
||||||
|
placeholder="Tambah data pasien terlebih dahulu"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- NIK -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="nik">NIK</Label>
|
||||||
|
<Field id="nik" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="nik">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="nik" v-bind="componentField" disabled placeholder="Otomatis" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label label-for="rm">No. RM</Label>
|
||||||
|
<Field id="rm" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="rm">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="rm" v-bind="componentField" disabled placeholder="RM99222" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Separator class="my-4 2xl:my-5" />
|
||||||
|
|
||||||
|
<div class="mb-2 2xl:mb-3 text-sm 2xl:text-base font-semibold">
|
||||||
|
Data Kunjungan
|
||||||
|
</div>
|
||||||
|
<Block :colCount="3">
|
||||||
|
<!-- Dokter (Combobox) -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="doctor_id">Dokter</Label>
|
||||||
|
<Field id="doctor_id" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="doctor_id">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox id="doctor_id" v-bind="componentField" :items="doctorOpts" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- Tanggal Daftar (DatePicker) -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="register_date">Tanggal Daftar</Label>
|
||||||
|
<Field id="register_date" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="register_date">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<DatepickerSingle v-bind="componentField" placeholder="Pilih tanggal" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- Jenis Pembayaran (Combobox) -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="payment_type">Jenis Pembayaran</Label>
|
||||||
|
<Field id="payment_type" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="payment_type">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<!-- <Combobox id="payment_type" v-bind="componentField" :items="paymentOpts" /> -->
|
||||||
|
<Select id="payment_type" v-bind="componentField" :items="paymentOpts" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :colCount="3">
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="bpjs_number">Kelompok Peserta</Label>
|
||||||
|
<Field id="bpjs_number" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="bpjs_number"
|
||||||
|
v-bind="componentField"
|
||||||
|
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- No. Kartu BPJS -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="bpjs_number">No. Kartu BPJS</Label>
|
||||||
|
<Field id="bpjs_number" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="bpjs_number">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="bpjs_number"
|
||||||
|
v-bind="componentField"
|
||||||
|
placeholder="Pilih jenis pembayaran terlebih dahulu"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- Jenis SEP -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="sep_type">Jenis SEP</Label>
|
||||||
|
<Field id="sep_type" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="sep_type">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select id="sep_type" v-bind="componentField" :items="sepOpts" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<Block :colCount="3">
|
||||||
|
<!-- No. SEP (input + tombol +) -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="sep_number">No. SEP</Label>
|
||||||
|
<Field id="sep_number" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="sep_number">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="sep_number"
|
||||||
|
v-bind="componentField"
|
||||||
|
placeholder="Tambah SEP terlebih dahulu"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Button class="bg-primary" size="sm" variant="outline" @click.prevent="onAddSep">+</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- Dokumen SEP (file) -->
|
||||||
|
<Cell :cosSpan="3">
|
||||||
|
<Label label-for="sep_file">Dokumen SEP</Label>
|
||||||
|
<Field id="sep_file" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="sep_file">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input ref="sepFileInput" type="file" class="hidden" @change="onSepFileChange" />
|
||||||
|
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSepFile"
|
||||||
|
>Pilih Berkas</Button
|
||||||
|
>
|
||||||
|
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SEP" />
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
|
||||||
|
<!-- Dokumen SIPP (file) -->
|
||||||
|
<Cell :cosSpan="3" labelSize="thin">
|
||||||
|
<Label label-for="sipp_file">Dokumen SIPP</Label>
|
||||||
|
<Field id="sipp_file" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="sipp_file">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input ref="sippFileInput" type="file" class="hidden" @change="onSippFileChange" />
|
||||||
|
<Button class="bg-primary" size="sm" variant="ghost" @click.prevent="pickSippFile"
|
||||||
|
>Pilih Berkas</Button
|
||||||
|
>
|
||||||
|
<Input readonly v-bind="componentField" placeholder="Unggah dokumen SIPP" />
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||||
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||||
|
import Button from '~/components/pub/ui/button/Button.vue'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { MedicalActionSrcFormData } from '~/schemas/medical-action-src.schema'
|
||||||
|
import { medicalActionTypeCode } from '~/lib/constants'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import type z from 'zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import { mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
schema: z.ZodSchema<any>
|
||||||
|
values: any
|
||||||
|
isLoading?: boolean
|
||||||
|
isReadonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isLoading = props.isLoading !== undefined ? props.isLoading : false
|
||||||
|
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
|
||||||
|
const medicalActionTypeOptions = mapToComboboxOptList(medicalActionTypeCode)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: MedicalActionSrcFormData, resetForm: () => void]
|
||||||
|
cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defineField, errors, meta } = useForm({
|
||||||
|
validationSchema: toTypedSchema(props.schema),
|
||||||
|
initialValues: {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
type_code: '',
|
||||||
|
} as Partial<MedicalActionSrcFormData>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [code, codeAttrs] = defineField('code')
|
||||||
|
const [name, nameAttrs] = defineField('name')
|
||||||
|
const [typeCode, typeCodeAttrs] = defineField('type_code')
|
||||||
|
|
||||||
|
if (props.values) {
|
||||||
|
if (props.values.code !== undefined) code.value = props.values.code
|
||||||
|
if (props.values.name !== undefined) name.value = props.values.name
|
||||||
|
if (props.values.type_code !== undefined) typeCode.value = props.values.type_code
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
code.value = ''
|
||||||
|
name.value = ''
|
||||||
|
typeCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitForm() {
|
||||||
|
const formData: MedicalActionSrcFormData = {
|
||||||
|
code: code.value || '',
|
||||||
|
name: name.value || '',
|
||||||
|
type_code: typeCode.value || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelForm() {
|
||||||
|
emit('cancel', resetForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
id="form-medical-action-src"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<Block
|
||||||
|
labelSize="thin"
|
||||||
|
class="!mb-2.5 !pt-0 xl:!mb-3"
|
||||||
|
:colCount="1"
|
||||||
|
>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Kode</Label>
|
||||||
|
<Field :errMessage="errors.code">
|
||||||
|
<Input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
v-bind="codeAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama</Label>
|
||||||
|
<Field :errMessage="errors.name">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="name"
|
||||||
|
v-bind="nameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Type Kode</Label>
|
||||||
|
<Field :errMessage="errors.name">
|
||||||
|
<Select
|
||||||
|
id="type_code"
|
||||||
|
:is-disabled="isLoading || isReadonly"
|
||||||
|
:items="medicalActionTypeOptions"
|
||||||
|
v-bind="typeCodeAttrs"
|
||||||
|
v-model="typeCode"
|
||||||
|
placeholder="Pilih medical action type"
|
||||||
|
:preserve-order="false"
|
||||||
|
class="text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<div class="my-2 flex justify-end gap-2 py-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-[120px]"
|
||||||
|
@click="onCancelForm"
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!isReadonly"
|
||||||
|
type="button"
|
||||||
|
class="w-[120px]"
|
||||||
|
:disabled="isLoading || !meta.valid"
|
||||||
|
@click="onSubmitForm"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, { width: 50 }],
|
||||||
|
|
||||||
|
headers: [[{ label: 'Kode' }, { label: 'Nama' }, { label: '' }]],
|
||||||
|
|
||||||
|
keys: ['code', 'name', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama ' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
|
// Configs
|
||||||
|
import { config } from './list-cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { Input } from '~/components/pub/ui/input'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
fieldNameInput: string
|
||||||
|
placeholder: string
|
||||||
|
labelForInput: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
maxLength?: number
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :col-span="2">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldNameInput"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ labelForInput }}
|
||||||
|
</DE.Label>
|
||||||
|
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldNameInput"
|
||||||
|
:errors="errors"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldNameInput"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
type="text"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { FormErrors } from '~/types/error'
|
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
|
||||||
import { Input } from '~/components/pub/ui/input'
|
|
||||||
import { cn } from '~/lib/utils'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
fieldNameAlias: string
|
|
||||||
fieldNameInput: string
|
|
||||||
placeholder: string
|
|
||||||
labelForAlias: string
|
|
||||||
labelForInput: string
|
|
||||||
errors?: FormErrors
|
|
||||||
class?: string
|
|
||||||
selectClass?: string
|
|
||||||
fieldGroupClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
maxLength?: number
|
|
||||||
isRequired?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const aliasOptions = [
|
|
||||||
{ label: 'An', value: 'an' },
|
|
||||||
{ label: 'By.Ny', value: 'byny' },
|
|
||||||
{ label: 'Nn', value: 'nn' },
|
|
||||||
{ label: 'Ny', value: 'ny' },
|
|
||||||
{ label: 'Tn', value: 'tn' },
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<FieldGroup>
|
|
||||||
<Label
|
|
||||||
:label-for="fieldNameAlias"
|
|
||||||
:is-required="isRequired"
|
|
||||||
>
|
|
||||||
{{ labelForAlias }}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
:id="fieldNameAlias"
|
|
||||||
:errors="errors"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
v-slot="{ componentField }"
|
|
||||||
:name="fieldNameAlias"
|
|
||||||
>
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
:id="fieldNameAlias"
|
|
||||||
:preserve-order="false"
|
|
||||||
v-bind="componentField"
|
|
||||||
:auto-width="true"
|
|
||||||
:items="aliasOptions"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
|
|
||||||
selectClass,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
<FieldGroup>
|
|
||||||
<Label
|
|
||||||
:label-for="fieldNameInput"
|
|
||||||
:is-required="isRequired"
|
|
||||||
>
|
|
||||||
{{ labelForInput }}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
:id="fieldNameInput"
|
|
||||||
:errors="errors"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
v-slot="{ componentField }"
|
|
||||||
:name="fieldNameInput"
|
|
||||||
>
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
v-bind="componentField"
|
|
||||||
type="text"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</template>
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -35,18 +34,19 @@ const genderOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('radio-group-field', containerClass)">
|
<DE.Cell :class="cn('radio-group-field', containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
height="compact"
|
height="compact"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
>
|
class="pt-0.5"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
:name="fieldName"
|
:name="fieldName"
|
||||||
@@ -67,7 +67,7 @@ const genderOptions = [
|
|||||||
:value="option.value"
|
:value="option.value"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
'relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
||||||
containerClass,
|
containerClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -76,7 +76,7 @@ const genderOptions = [
|
|||||||
:for="`${fieldName}-${index}`"
|
:for="`${fieldName}-${index}`"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
'cursor-pointer select-none font-normal text-xs leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||||
labelClass,
|
labelClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -89,6 +89,6 @@ const genderOptions = [
|
|||||||
<FormMessage class="ml-0 mt-1" />
|
<FormMessage class="ml-0 mt-1" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -35,17 +34,18 @@ const dissabilityOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('radio-group-field', containerClass)">
|
<DE.Cell :class="cn('radio-group-field', containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
height="compact"
|
height="compact"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
|
class="pt-0.5"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
@@ -67,7 +67,7 @@ const dissabilityOptions = [
|
|||||||
:value="option.value"
|
:value="option.value"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
'relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
||||||
containerClass,
|
containerClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -76,7 +76,7 @@ const dissabilityOptions = [
|
|||||||
:for="`${fieldName}-${index}`"
|
:for="`${fieldName}-${index}`"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||||
labelClass,
|
labelClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -89,6 +89,6 @@ const dissabilityOptions = [
|
|||||||
<FormMessage class="ml-0 mt-1" />
|
<FormMessage class="ml-0 mt-1" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
import { genderCodes } from '~/lib/constants'
|
import { genderCodes } from '~/lib/constants'
|
||||||
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -33,15 +32,15 @@ const genderOptions = mapToComboboxOptList(genderCodes)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('radio-group-field', containerClass)">
|
<DE.Cell :class="cn('radio-group-field', containerClass)" :col-span="4">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
height="compact"
|
height="compact"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
>
|
>
|
||||||
@@ -65,7 +64,7 @@ const genderOptions = mapToComboboxOptList(genderCodes)
|
|||||||
:value="option.value"
|
:value="option.value"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
'relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
||||||
containerClass,
|
containerClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -74,7 +73,7 @@ const genderOptions = mapToComboboxOptList(genderCodes)
|
|||||||
:for="`${fieldName}-${index}`"
|
:for="`${fieldName}-${index}`"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
'cursor-pointer select-none text-xs 2xl:text-sm leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 !font-normal',
|
||||||
labelClass,
|
labelClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -87,6 +86,6 @@ const genderOptions = mapToComboboxOptList(genderCodes)
|
|||||||
<FormMessage class="ml-0 mt-1" />
|
<FormMessage class="ml-0 mt-1" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -35,17 +34,18 @@ const nationalityOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('radio-group-field', containerClass)">
|
<DE.Cell :class="cn('radio-group-field', containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
height="compact"
|
height="compact"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
|
class="pt-0.5"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
@@ -67,7 +67,7 @@ const nationalityOptions = [
|
|||||||
:value="option.value"
|
:value="option.value"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
'relative h-4 w-4 rounded-full border-1 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
||||||
containerClass,
|
containerClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -76,7 +76,7 @@ const nationalityOptions = [
|
|||||||
:for="`${fieldName}-${index}`"
|
:for="`${fieldName}-${index}`"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
'cursor-pointer select-none text-xs !font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||||
labelClass,
|
labelClass,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -89,6 +89,6 @@ const nationalityOptions = [
|
|||||||
<FormMessage class="ml-0 mt-1" />
|
<FormMessage class="ml-0 mt-1" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
radioGroupClass?: string
|
||||||
|
radioItemClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'isNewBorn',
|
||||||
|
label = 'Status Pasien',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
radioGroupClass,
|
||||||
|
radioItemClass,
|
||||||
|
labelClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const newbornOptions = [
|
||||||
|
{ label: 'Ya', value: 'YA' },
|
||||||
|
{ label: 'Tidak', value: 'TIDAK' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('radio-group-field', containerClass)" :col-span="2">
|
||||||
|
<DE.Label
|
||||||
|
:label-for="fieldName"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
v-bind="componentField"
|
||||||
|
:class="cn('flex flex-row flex-wrap gap-4 sm:gap-6', radioGroupClass)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in newbornOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:class="cn('flex min-w-fit items-center space-x-2 pt-1', radioItemClass)"
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
:id="`${fieldName}-${index}`"
|
||||||
|
:value="option.value"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative h-4 w-4 rounded-full border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5',
|
||||||
|
containerClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<RadioLabel
|
||||||
|
:for="`${fieldName}-${index}`"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'cursor-pointer select-none text-xs font-normal leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm',
|
||||||
|
labelClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</RadioLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage class="ml-0 mt-1" />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -40,14 +39,14 @@ const disabilityOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
label-for="fieldName"
|
label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -76,6 +75,6 @@ const disabilityOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
|
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { Input } from '~/components/pub/ui/input'
|
import { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -76,15 +75,15 @@ function calculateAge(birthDate: string | Date | undefined): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -113,12 +112,11 @@ function calculateAge(birthDate: string | Date | undefined): string {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
<FieldGroup>
|
<DE.Cell>
|
||||||
<Label label-for="patientAge">Usia</Label>
|
<DE.Label label-for="patientAge">Usia</DE.Label>
|
||||||
|
<DE.Field id="patientAge">
|
||||||
<Field id="patientAge">
|
|
||||||
<FormField name="patientAge">
|
<FormField name="patientAge">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -137,6 +135,6 @@ function calculateAge(birthDate: string | Date | undefined): string {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { educationCodes } from '~/lib/constants'
|
import { educationCodes } from '~/lib/constants'
|
||||||
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -46,15 +45,15 @@ const educationOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -83,6 +82,6 @@ const educationOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -49,15 +48,15 @@ const ethnicOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -81,6 +80,6 @@ const ethnicOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
label?: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
isRequired?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'disabilityType',
|
||||||
|
placeholder = 'Pilih jenis disabilitas',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
selectClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const disabilityOptions = [
|
||||||
|
{ label: 'Laki', value: 'male' },
|
||||||
|
{ label: 'Perempuan', value: 'female' },
|
||||||
|
{ label: 'Tidak Disebutkan', value: 'not-stated' },
|
||||||
|
{ label: 'Tidak Diketahui', value: 'unknown' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label
|
||||||
|
label-for="fieldName"
|
||||||
|
:is-required="isRequired"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
:class="cn('select-field-wrapper')"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
:id="fieldName"
|
||||||
|
:is-disabled="isDisabled"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="disabilityOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:preserve-order="false"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'text-sm transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-0',
|
||||||
|
selectClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
import { occupationCodes } from '~/lib/constants'
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import { cn } from '~/lib/utils'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
@@ -28,106 +28,20 @@ const {
|
|||||||
labelClass,
|
labelClass,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const jobOptions = [
|
// Generate job options from constants, sama seperti pola genderCodes
|
||||||
{ label: 'Tidak diketahui', value: 'unknown', priority: 100 },
|
const jobOptions = mapToComboboxOptList(occupationCodes)
|
||||||
{ label: 'Belum/Tidak Bekerja', value: 'tidak_bekerja', priority: 99 },
|
|
||||||
{ label: 'Mengurus Rumah Tangga', value: 'mengurus_rumah_tangga' },
|
|
||||||
{ label: 'Pelajar/Mahasiswa', value: 'pelajar' },
|
|
||||||
{ label: 'Pensiunan', value: 'pensiunan' },
|
|
||||||
{ label: 'Pegawai Negeri Sipil', value: 'pns' },
|
|
||||||
{ label: 'Tentara Nasional Indonesia', value: 'tni' },
|
|
||||||
{ label: 'Kepolisian RI', value: 'polri' },
|
|
||||||
{ label: 'Perdagangan', value: 'perdagangan' },
|
|
||||||
{ label: 'Petani/Pekebun', value: 'petani' },
|
|
||||||
{ label: 'Peternak', value: 'peternak' },
|
|
||||||
{ label: 'Nelayan/Perikanan', value: 'nelayan' },
|
|
||||||
{ label: 'Industri', value: 'industri' },
|
|
||||||
{ label: 'Konstruksi', value: 'konstruksi' },
|
|
||||||
{ label: 'Transportasi', value: 'transportasi' },
|
|
||||||
{ label: 'Karyawan Swasta', value: 'karyawan_swasta' },
|
|
||||||
{ label: 'Karyawan BUMN', value: 'karyawan_bumn' },
|
|
||||||
{ label: 'Karyawan BUMD', value: 'karyawan_bumd' },
|
|
||||||
{ label: 'Karyawan Honorer', value: 'karyawan_honorer' },
|
|
||||||
{ label: 'Buruh Harian Lepas', value: 'buruh_harian' },
|
|
||||||
{ label: 'Buruh Tani/Perkebunan', value: 'buruh_tani' },
|
|
||||||
{ label: 'Buruh Nelayan/Perikanan', value: 'buruh_nelayan' },
|
|
||||||
{ label: 'Buruh Peternakan', value: 'buruh_peternakan' },
|
|
||||||
{ label: 'Pembantu Rumah Tangga', value: 'pembantu_rumah_tangga' },
|
|
||||||
{ label: 'Tukang Cukur', value: 'tukang_cukur' },
|
|
||||||
{ label: 'Tukang Listrik', value: 'tukang_listrik' },
|
|
||||||
{ label: 'Tukang Batu', value: 'tukang_batu' },
|
|
||||||
{ label: 'Tukang Kayu', value: 'tukang_kayu' },
|
|
||||||
{ label: 'Tukang Sol Sepatu', value: 'tukang_sol_sepatu' },
|
|
||||||
{ label: 'Tukang Jahit', value: 'tukang_jahit' },
|
|
||||||
{ label: 'Tukang Gigi', value: 'tukang_gigi' },
|
|
||||||
{ label: 'Penata Rias', value: 'penata_rias' },
|
|
||||||
{ label: 'Penata Busana', value: 'penata_busana' },
|
|
||||||
{ label: 'Penata Rambut', value: 'penata_rambut' },
|
|
||||||
{ label: 'Mekanik', value: 'mekanik' },
|
|
||||||
{ label: 'Seniman', value: 'seniman' },
|
|
||||||
{ label: 'Tabib', value: 'tabib' },
|
|
||||||
{ label: 'Paraji', value: 'paraji' },
|
|
||||||
{ label: 'Perancang Busana', value: 'perancang_busana' },
|
|
||||||
{ label: 'Penterjemah', value: 'penterjemah' },
|
|
||||||
{ label: 'Imam Mesjid', value: 'imam_mesjid' },
|
|
||||||
{ label: 'Pendeta', value: 'pendeta' },
|
|
||||||
{ label: 'Pastor', value: 'pastor' },
|
|
||||||
{ label: 'Wartawan', value: 'wartawan' },
|
|
||||||
{ label: 'Ustadz/Mubaligh', value: 'ustadz' },
|
|
||||||
{ label: 'Juru Masak', value: 'juru_masak' },
|
|
||||||
{ label: 'Promotor Acara', value: 'promotor' },
|
|
||||||
{ label: 'Anggota DPR-RI', value: 'dpr_ri' },
|
|
||||||
{ label: 'Anggota DPD', value: 'dpd' },
|
|
||||||
{ label: 'Anggota BPK', value: 'bpk' },
|
|
||||||
{ label: 'Presiden', value: 'presiden' },
|
|
||||||
{ label: 'Wakil Presiden', value: 'wakil_presiden' },
|
|
||||||
{ label: 'Anggota Mahkamah Konstitusi', value: 'mk' },
|
|
||||||
{ label: 'Anggota Kabinet/Kementrian', value: 'kabinet' },
|
|
||||||
{ label: 'Duta Besar', value: 'dubes' },
|
|
||||||
{ label: 'Gubernur', value: 'gubernur' },
|
|
||||||
{ label: 'Wakil Gubernur', value: 'wakil_gubernur' },
|
|
||||||
{ label: 'Bupati', value: 'bupati' },
|
|
||||||
{ label: 'Wakil Bupati', value: 'wakil_bupati' },
|
|
||||||
{ label: 'Walikota', value: 'walikota' },
|
|
||||||
{ label: 'Wakil Walikota', value: 'wakil_walikota' },
|
|
||||||
{ label: 'Anggota DPRD Provinsi', value: 'dprd_provinsi' },
|
|
||||||
{ label: 'Anggota DPRD Kabupaten/Kota', value: 'dprd_kabkota' },
|
|
||||||
{ label: 'Dosen', value: 'dosen' },
|
|
||||||
{ label: 'Guru', value: 'guru' },
|
|
||||||
{ label: 'Pilot', value: 'pilot' },
|
|
||||||
{ label: 'Pengacara', value: 'pengacara' },
|
|
||||||
{ label: 'Arsitek', value: 'arsitek' },
|
|
||||||
{ label: 'Akuntan', value: 'akuntan' },
|
|
||||||
{ label: 'Konsultan', value: 'konsultan' },
|
|
||||||
{ label: 'Dokter', value: 'dokter' },
|
|
||||||
{ label: 'Bidan', value: 'bidan' },
|
|
||||||
{ label: 'Apoteker', value: 'apoteker' },
|
|
||||||
{ label: 'Psikiater/Psikolog', value: 'psikolog' },
|
|
||||||
{ label: 'Penyiar Televisi', value: 'penyiar_tv' },
|
|
||||||
{ label: 'Penyiar Radio', value: 'penyiar_radio' },
|
|
||||||
{ label: 'Pelaut', value: 'pelaut' },
|
|
||||||
{ label: 'Sopir', value: 'sopir' },
|
|
||||||
{ label: 'Pialang', value: 'pialang' },
|
|
||||||
{ label: 'Paranormal', value: 'paranormal' },
|
|
||||||
{ label: 'Pedagang', value: 'pedagang' },
|
|
||||||
{ label: 'Perangkat Desa', value: 'perangkat_desa' },
|
|
||||||
{ label: 'Kepala Desa', value: 'kepala_desa' },
|
|
||||||
{ label: 'Biarawati', value: 'biarawati' },
|
|
||||||
{ label: 'Wiraswasta', value: 'wiraswasta' },
|
|
||||||
{ label: 'Lainnya', value: 'lainnya', priority: -100 },
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -150,6 +64,6 @@ const jobOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -41,15 +40,15 @@ const langOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -77,6 +76,6 @@ const langOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -37,15 +36,15 @@ const maritalStatusOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -73,6 +72,6 @@ const maritalStatusOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
import type { FormErrors } from '~/types/error'
|
||||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|
||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { religionCodes } from '~/lib/constants'
|
import { religionCodes } from '~/lib/constants'
|
||||||
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
@@ -45,15 +44,15 @@ const religionOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:class="cn('select-field-label', labelClass)"
|
:class="cn('select-field-label', labelClass)"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -81,6 +80,6 @@ const religionOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,12 +3,14 @@ import type { FormErrors } from '~/types/error'
|
|||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { Form } from '~/components/pub/ui/form'
|
import { Form } from '~/components/pub/ui/form'
|
||||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
import InputFile from './_common/input-file.vue'
|
import FileUpload from '~/components/pub/my-ui/form/file-field.vue'
|
||||||
import InputPatientName from './_common/input-patient-name.vue'
|
import InputName from './_common/input-name.vue'
|
||||||
import RadioCommunicationBarrier from './_common/radio-communication-barrier.vue'
|
import RadioCommunicationBarrier from './_common/radio-communication-barrier.vue'
|
||||||
import RadioDisability from './_common/radio-disability.vue'
|
import RadioDisability from './_common/radio-disability.vue'
|
||||||
import RadioGender from './_common/radio-gender.vue'
|
import SelectGender from './_common/select-gender.vue'
|
||||||
import RadioNationality from './_common/radio-nationality.vue'
|
import RadioNationality from './_common/radio-nationality.vue'
|
||||||
|
import RadioNewborn from './_common/radio-newborn.vue'
|
||||||
|
import SelectBirthPlace from '~/components/app/person/_common/select-birth-place.vue'
|
||||||
import SelectDisability from './_common/select-disability.vue'
|
import SelectDisability from './_common/select-disability.vue'
|
||||||
import SelectDob from './_common/select-dob.vue'
|
import SelectDob from './_common/select-dob.vue'
|
||||||
import SelectEducation from './_common/select-education.vue'
|
import SelectEducation from './_common/select-education.vue'
|
||||||
@@ -17,6 +19,9 @@ import SelectJob from './_common/select-job.vue'
|
|||||||
import SelectLanguage from './_common/select-lang.vue'
|
import SelectLanguage from './_common/select-lang.vue'
|
||||||
import SelectMaritalStatus from './_common/select-marital-status.vue'
|
import SelectMaritalStatus from './_common/select-marital-status.vue'
|
||||||
import SelectReligion from './_common/select-religion.vue'
|
import SelectReligion from './_common/select-religion.vue'
|
||||||
|
import Separator from '~/components/pub/ui/separator/Separator.vue'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
schema: any
|
schema: any
|
||||||
@@ -46,180 +51,161 @@ defineExpose({
|
|||||||
validation-mode="onSubmit"
|
validation-mode="onSubmit"
|
||||||
:initial-values="initialValues ? initialValues : {}"
|
:initial-values="initialValues ? initialValues : {}"
|
||||||
>
|
>
|
||||||
<div class="mb-3 border-b border-b-slate-300">
|
<p class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold">Data Diri Pasien</p>
|
||||||
<p class="text-md mt-1 font-semibold">Data Diri Pasien</p>
|
<DE.Block :col-count="4" :cell-flex="false">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-[150px_1fr]">
|
<InputBase
|
||||||
<InputPatientName
|
field-name="identityNumber"
|
||||||
field-name-alias="alias"
|
label="No. KTP"
|
||||||
field-name-input="fullName"
|
placeholder="Masukkan NIK"
|
||||||
label-for-alias="Alias"
|
:errors="errors"
|
||||||
label-for-input="Nama Lengkap"
|
numeric-only
|
||||||
placeholder="Masukkan nama lengkap pasien"
|
/>
|
||||||
:errors="errors"
|
<InputBase
|
||||||
is-required
|
field-name="drivingLicenseNumber"
|
||||||
/>
|
label="No. SIM"
|
||||||
</div>
|
placeholder="Masukkan nomor SIM"
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
numeric-only
|
||||||
<InputBase
|
:max-length="20"
|
||||||
field-name="birthPlace"
|
:errors="errors"
|
||||||
label="Tempat Lahir"
|
/>
|
||||||
placeholder="Malang"
|
<InputBase
|
||||||
:errors="errors"
|
field-name="passportNumber"
|
||||||
is-required
|
label="No. Paspor"
|
||||||
/>
|
placeholder="Masukkan nomor paspor"
|
||||||
<SelectDob
|
:max-length="20"
|
||||||
label="Tanggal Lahir"
|
:errors="errors"
|
||||||
:errors="errors"
|
/>
|
||||||
is-required
|
<InputName
|
||||||
/>
|
field-name-alias="alias"
|
||||||
</div>
|
field-name-input="fullName"
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
label-for-input="Nama Lengkap"
|
||||||
<RadioGender
|
placeholder="Masukkan nama lengkap pasien"
|
||||||
field-name="gender"
|
:errors="errors"
|
||||||
label="Jenis Kelamin"
|
is-required
|
||||||
placeholder="Pilih jenis kelamin"
|
/>
|
||||||
:errors="errors"
|
<RadioNewborn
|
||||||
is-required
|
field-name="isNewBorn"
|
||||||
/>
|
label="Pasien Bayi"
|
||||||
<RadioNationality
|
placeholder="Pilih status pasien"
|
||||||
field-name="nationality"
|
:errors="errors"
|
||||||
label="Kebangsaan"
|
is-required
|
||||||
placeholder="Pilih kebangsaan"
|
/>
|
||||||
:errors="errors"
|
<SelectGender
|
||||||
is-required
|
field-name="gender"
|
||||||
/>
|
label="Jenis Kelamin"
|
||||||
</div>
|
placeholder="Pilih jenis kelamin"
|
||||||
</div>
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectBirthPlace
|
||||||
|
field-name="birthPlace"
|
||||||
|
label="Tempat Lahir"
|
||||||
|
placeholder="Pilih tempat lahir"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDob
|
||||||
|
label="Tanggal Lahir"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectEducation
|
||||||
|
field-name="education"
|
||||||
|
label="Pendidikan"
|
||||||
|
placeholder="Pilih pendidikan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectJob
|
||||||
|
field-name="job"
|
||||||
|
label="Pekerjaan"
|
||||||
|
placeholder="Pilih pekerjaan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectMaritalStatus
|
||||||
|
field-name="maritalStatus"
|
||||||
|
label="Status Perkawinan"
|
||||||
|
placeholder="Pilih status Perkawinan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<DE.Cell />
|
||||||
|
<RadioNationality
|
||||||
|
field-name="nationality"
|
||||||
|
label="Kebangsaan"
|
||||||
|
placeholder="Pilih kebangsaan"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectEthnicity
|
||||||
|
field-name="ethnicity"
|
||||||
|
label="Suku"
|
||||||
|
placeholder="Pilih suku bangsa"
|
||||||
|
:errors="errors"
|
||||||
|
:is-disabled="values.nationality !== 'WNI'"
|
||||||
|
/>
|
||||||
|
<SelectLanguage
|
||||||
|
field-name="language"
|
||||||
|
label="Bahasa"
|
||||||
|
placeholder="Pilih preferensi bahasa"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectReligion
|
||||||
|
field-name="religion"
|
||||||
|
label="Agama"
|
||||||
|
placeholder="Pilih agama"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<RadioCommunicationBarrier
|
||||||
|
field-name="communicationBarrier"
|
||||||
|
label="Hambatan Berkomunikasi"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<RadioDisability
|
||||||
|
field-name="disability"
|
||||||
|
label="Disabilitas"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDisability
|
||||||
|
label="Jenis Disabilitas"
|
||||||
|
field-name="disabilityType"
|
||||||
|
:errors="errors"
|
||||||
|
:is-disabled="values.disability !== 'YA'"
|
||||||
|
:is-required="values.disability === 'YA'"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="note"
|
||||||
|
label="Kepercayaan"
|
||||||
|
placeholder="Contoh: tidak ingin diperiksa oleh dokter laki-laki"
|
||||||
|
:errors="errors"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
|
||||||
<div class="mb-3 border-b border-b-slate-300">
|
<div class="h-6"></div>
|
||||||
<p class="text-md mt-1 font-semibold">Dokumen Identitas</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
<p class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold">Dokumen Identitas</p>
|
||||||
<InputBase
|
<DE.Block :col-count="2" :cell-flex="false">
|
||||||
field-name="identityNumber"
|
<FileUpload
|
||||||
label="No. KTP"
|
field-name="identityCardFile"
|
||||||
placeholder="Masukkan NIK"
|
label="Dokumen KTP"
|
||||||
:errors="errors"
|
placeholder="Unggah scan dokumen KTP"
|
||||||
numeric-only
|
:errors="errors"
|
||||||
:max-length="16"
|
:accept="['pdf', 'jpg', 'png']"
|
||||||
is-required
|
:max-size-mb="1"
|
||||||
/>
|
/>
|
||||||
<InputBase
|
<FileUpload
|
||||||
field-name="drivingLicenseNumber"
|
field-name="familyCardFile"
|
||||||
label="No. SIM"
|
label="Dokumen KK"
|
||||||
placeholder="Masukkan nomor SIM"
|
placeholder="Unggah scan dokumen KK"
|
||||||
numeric-only
|
:errors="errors"
|
||||||
:max-length="20"
|
:accept="['pdf', 'jpg', 'png']"
|
||||||
:errors="errors"
|
:max-size-mb="1"
|
||||||
/>
|
/>
|
||||||
|
</DE.Block>
|
||||||
<InputBase
|
|
||||||
field-name="passportNumber"
|
|
||||||
label="No. Paspor"
|
|
||||||
placeholder="Masukkan nomor paspor"
|
|
||||||
:max-length="20"
|
|
||||||
:errors="errors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
|
||||||
<InputFile
|
|
||||||
field-name="identityCardFile"
|
|
||||||
label="Dokumen KTP"
|
|
||||||
placeholder="Unggah scan dokumen KTP"
|
|
||||||
:errors="errors"
|
|
||||||
/>
|
|
||||||
<InputFile
|
|
||||||
field-name="familyCardFile"
|
|
||||||
label="Dokumen KK"
|
|
||||||
placeholder="Unggah scan dokumen KK"
|
|
||||||
:errors="errors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 border-b border-b-slate-300">
|
|
||||||
<p class="text-md mt-1 font-semibold">Data Demografis</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
|
||||||
<SelectReligion
|
|
||||||
field-name="religion"
|
|
||||||
label="Agama"
|
|
||||||
placeholder="Pilih agama"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
<SelectEthnicity
|
|
||||||
field-name="ethnicity"
|
|
||||||
label="Suku"
|
|
||||||
placeholder="Pilih suku bangsa"
|
|
||||||
:errors="errors"
|
|
||||||
:is-disabled="values.nationality !== 'WNI'"
|
|
||||||
/>
|
|
||||||
<SelectLanguage
|
|
||||||
field-name="language"
|
|
||||||
label="Bahasa"
|
|
||||||
placeholder="Pilih preferensi bahasa"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
|
||||||
<SelectMaritalStatus
|
|
||||||
field-name="maritalStatus"
|
|
||||||
label="Status Perkawinan"
|
|
||||||
placeholder="Pilih status Perkawinan"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
<SelectEducation
|
|
||||||
field-name="education"
|
|
||||||
label="Pendidikan"
|
|
||||||
placeholder="Pilih pendidikan"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
<SelectJob
|
|
||||||
field-name="job"
|
|
||||||
label="Pekerjaan"
|
|
||||||
placeholder="Pilih pekerjaan"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 border-b border-b-slate-300">
|
|
||||||
<p class="text-md mt-1 font-semibold">Kondisi Khusus</p>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
|
||||||
<RadioCommunicationBarrier
|
|
||||||
field-name="communicationBarrier"
|
|
||||||
label="Hambatan Berkomunikasi"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
<div class="cols-span-1">
|
|
||||||
<RadioDisability
|
|
||||||
field-name="disability"
|
|
||||||
label="Disabilitas"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
<SelectDisability
|
|
||||||
label="Jenis Disabilitas"
|
|
||||||
field-name="disabilityType"
|
|
||||||
:errors="errors"
|
|
||||||
:is-disabled="values.disability !== 'YA'"
|
|
||||||
:is-required="values.disability === 'YA'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<InputBase
|
|
||||||
field-name="note"
|
|
||||||
label="Kepercayaan"
|
|
||||||
placeholder="Contoh: tidak ingin diperiksa oleh dokter laki-laki"
|
|
||||||
:errors="errors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Config } from '~/components/pub/my-ui/data-table'
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
import type { PatientEntity } from '~/models/patient'
|
import type { Patient } from '~/models/patient'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import { educationCodes, genderCodes } from '~/lib/constants'
|
import { educationCodes, genderCodes } from '~/lib/constants'
|
||||||
import { calculateAge } from '~/lib/utils'
|
import { calculateAge } from '~/lib/utils'
|
||||||
@@ -11,8 +11,9 @@ export const config: Config = {
|
|||||||
|
|
||||||
headers: [
|
headers: [
|
||||||
[
|
[
|
||||||
|
{ label: 'ID' },
|
||||||
{ label: 'Nama' },
|
{ label: 'Nama' },
|
||||||
{ label: 'NIK' },
|
{ label: 'NIK/No. Paspor' },
|
||||||
{ label: 'Tgl Lahir' },
|
{ label: 'Tgl Lahir' },
|
||||||
{ label: 'Umur' },
|
{ label: 'Umur' },
|
||||||
{ label: 'Jenis Kelamin' },
|
{ label: 'Jenis Kelamin' },
|
||||||
@@ -21,15 +22,7 @@ export const config: Config = {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
keys: [
|
keys: ['patientId', 'name', 'identity_number', 'birth_date', 'patient_age', 'gender', 'education', 'action'],
|
||||||
'name',
|
|
||||||
'identity_number',
|
|
||||||
'birth_date',
|
|
||||||
'patient_age',
|
|
||||||
'gender',
|
|
||||||
'education',
|
|
||||||
'action',
|
|
||||||
],
|
|
||||||
|
|
||||||
delKeyNames: [
|
delKeyNames: [
|
||||||
{ key: 'code', label: 'Kode' },
|
{ key: 'code', label: 'Kode' },
|
||||||
@@ -37,33 +30,41 @@ export const config: Config = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
parses: {
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
name: (rec: unknown): unknown => {
|
name: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
return person.name.trim()
|
return person.name.trim()
|
||||||
},
|
},
|
||||||
identity_number: (rec: unknown): unknown => {
|
identity_number: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
if (person?.residentIdentityNumber?.substring(0, 5) === 'BLANK') {
|
|
||||||
return '(TANPA NIK)'
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
}
|
}
|
||||||
return person.residentIdentityNumber
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
},
|
},
|
||||||
birth_date: (rec: unknown): unknown => {
|
birth_date: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
if (typeof person.birthDate === 'object' && person.birthDate) {
|
|
||||||
return (person.birthDate as Date).toLocaleDateString()
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
} else if (typeof person.birthDate === 'string') {
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
return person.birthDate.substring(0, 10)
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
}
|
}
|
||||||
return person.birthDate
|
return person.birthDate
|
||||||
},
|
},
|
||||||
patient_age: (rec: unknown): unknown => {
|
patient_age: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
return calculateAge(person.birthDate)
|
return calculateAge(person.birthDate)
|
||||||
},
|
},
|
||||||
gender: (rec: unknown): unknown => {
|
gender: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
if (typeof person.gender_code === 'number' && person.gender_code >= 0) {
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
return person.gender_code
|
return person.gender_code
|
||||||
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
return genderCodes[person.gender_code] || '-'
|
return genderCodes[person.gender_code] || '-'
|
||||||
@@ -71,8 +72,8 @@ export const config: Config = {
|
|||||||
return '-'
|
return '-'
|
||||||
},
|
},
|
||||||
education: (rec: unknown): unknown => {
|
education: (rec: unknown): unknown => {
|
||||||
const { person } = rec as PatientEntity
|
const { person } = rec as Patient
|
||||||
if (typeof person.education_code === 'number' && person.education_code >= 0) {
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
return person.education_code
|
return person.education_code
|
||||||
} else if (typeof person.education_code === 'string' && person.education_code) {
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
return educationCodes[person.education_code] || '-'
|
return educationCodes[person.education_code] || '-'
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { Config } from '~/components/pub/my-ui/data-table'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
import { educationCodes, genderCodes } from '~/lib/constants'
|
||||||
|
import { calculateAge } from '~/lib/utils'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, {}, {}, {}, {}, { width: 5 }],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'No. RM' },
|
||||||
|
{ label: 'Nama' },
|
||||||
|
{ label: 'No. KTP/SIM/Passpor' },
|
||||||
|
{ label: 'Tgl Lahir' },
|
||||||
|
{ label: 'Umur' },
|
||||||
|
{ label: 'Kelamin' },
|
||||||
|
{ label: 'Pendidikan' },
|
||||||
|
{ label: '' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['number', 'person.name', 'identity_number', 'birth_date', 'patient_age', 'gender', 'education', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
|
identity_number: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
|
},
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
patient_age: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
return calculateAge(person.birthDate)
|
||||||
|
},
|
||||||
|
gender: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
|
return person.gender_code
|
||||||
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
|
return genderCodes[person.gender_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
education: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
|
return person.education_code
|
||||||
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
|
return educationCodes[person.education_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
import { config } from './list-cfg'
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any[]
|
data: any[]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { config } from './list-cfg'
|
import { config } from './list.cfg'
|
||||||
|
|
||||||
defineProps<{ data: any[] }>()
|
defineProps<{ data: any[] }>()
|
||||||
const modelValue = defineModel<any | null>()
|
const modelValue = defineModel<any | null>()
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Person } from '~/models/person'
|
import type { Patient } from '~/models/patient'
|
||||||
import type { PersonAddress } from '~/models/person-address'
|
|
||||||
import type { PersonContact } from '~/models/person-contact'
|
|
||||||
import type { PersonRelative } from '~/models/person-relative'
|
|
||||||
|
|
||||||
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||||
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
|
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
|
||||||
|
import { formatAddress } from '~/models/person-address'
|
||||||
|
|
||||||
import { educationCodes, genderCodes, personContactTypes, relationshipCodes, religionCodes } from '~/lib/constants'
|
import {
|
||||||
|
addressLocationTypeCode,
|
||||||
|
educationCodes,
|
||||||
|
genderCodes,
|
||||||
|
occupationCodes,
|
||||||
|
personContactTypes,
|
||||||
|
relationshipCodes,
|
||||||
|
religionCodes,
|
||||||
|
} from '~/lib/constants'
|
||||||
import { mapToComboboxOptList } from '~/lib/utils'
|
import { mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
// #region Props & Emits
|
// #region Props & Emits
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
person: Person
|
patient: Patient
|
||||||
personAddresses: PersonAddress[]
|
|
||||||
personContacts: PersonContact[]
|
|
||||||
personRelatives: PersonRelative[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -28,17 +30,28 @@ const emit = defineEmits<{
|
|||||||
const genderOptions = mapToComboboxOptList(genderCodes)
|
const genderOptions = mapToComboboxOptList(genderCodes)
|
||||||
const religionOptions = mapToComboboxOptList(religionCodes)
|
const religionOptions = mapToComboboxOptList(religionCodes)
|
||||||
const educationOptions = mapToComboboxOptList(educationCodes)
|
const educationOptions = mapToComboboxOptList(educationCodes)
|
||||||
|
const occupationOptions = mapToComboboxOptList(occupationCodes)
|
||||||
const relationshipOptions = mapToComboboxOptList(relationshipCodes)
|
const relationshipOptions = mapToComboboxOptList(relationshipCodes)
|
||||||
const personContactTypeOptions = mapToComboboxOptList(personContactTypes)
|
const personContactTypeOptions = mapToComboboxOptList(personContactTypes)
|
||||||
|
|
||||||
const residentAddress = 'Jl. Puncak Borobudur Blok M No. 321, Lowokwaru, Kota Malang, Jawa Timur'
|
// Computed addresses from nested data
|
||||||
const primaryAddress = 'Perumahan Araya Cluster B, No 22, Blimbing, Kota Malang, Jawa Timur'
|
const domicileAddress = computed(() => {
|
||||||
|
const addresses = props.patient.person.addresses
|
||||||
|
const resident = addresses?.find((addr) => addr.locationType_code === 'domicile')
|
||||||
|
return formatAddress(resident)
|
||||||
|
})
|
||||||
|
|
||||||
|
const identityAddress = computed(() => {
|
||||||
|
const addresses = props.patient.person.addresses
|
||||||
|
const primary = addresses?.find((addr) => addr.locationType_code === 'identity')
|
||||||
|
return formatAddress(primary)
|
||||||
|
})
|
||||||
|
|
||||||
const patientAge = computed(() => {
|
const patientAge = computed(() => {
|
||||||
if (!props.person.birthDate) {
|
if (!props.patient.person.birthDate) {
|
||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
const birthDate = new Date(props.person.birthDate)
|
const birthDate = new Date(props.patient.person.birthDate)
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
let age = today.getFullYear() - birthDate.getFullYear()
|
let age = today.getFullYear() - birthDate.getFullYear()
|
||||||
const monthDiff = today.getMonth() - birthDate.getMonth()
|
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||||
@@ -53,6 +66,7 @@ const patientAge = computed(() => {
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Functions
|
// #region Functions
|
||||||
|
|
||||||
// #endregion region
|
// #endregion region
|
||||||
|
|
||||||
// #region Utilities & event handlers
|
// #region Utilities & event handlers
|
||||||
@@ -67,48 +81,53 @@ function onClick(type: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DetailSection title="Data Pasien">
|
<DetailSection title="Data Pasien">
|
||||||
<DetailRow label="Nomor ID">{{ person.id || '-' }}</DetailRow>
|
<DetailRow label="Nomor">{{ patient.number || '-' }}</DetailRow>
|
||||||
<DetailRow label="Sapaan">{{ person.alias || '-' }}</DetailRow>
|
<DetailRow label="Nama Lengkap">{{ patient.person.name || '-' }}</DetailRow>
|
||||||
<DetailRow label="Nama Lengkap">{{ person.name || '-' }}</DetailRow>
|
|
||||||
<DetailRow label="Tempat, tanggal lahir">
|
<DetailRow label="Tempat, tanggal lahir">
|
||||||
{{ person.birthRegency_code || '-' }},
|
{{ patient.person.birthRegency?.name || '-' }},
|
||||||
{{ person.birthDate ? new Date(person.birthDate).toLocaleDateString() : '-' }}
|
{{ patient.person.birthDate ? new Date(patient.person.birthDate).toLocaleDateString('id-ID') : '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Usia">{{ patientAge || '-' }}</DetailRow>
|
<DetailRow label="Usia">{{ patientAge || '-' }} Tahun</DetailRow>
|
||||||
<DetailRow label="Tanggal Daftar">
|
<DetailRow label="Tanggal Daftar">
|
||||||
{{ person.createdAt ? new Date(person.createdAt).toLocaleDateString() : '-' }}
|
{{ patient.person.createdAt ? new Date(patient.person.createdAt).toLocaleDateString('id-ID') : '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Jenis Kelamin">
|
<DetailRow label="Jenis Kelamin">
|
||||||
{{ genderOptions.find((item) => item.code === person.gender_code)?.label || '-' }}
|
{{ genderOptions.find((item) => item.code === patient.person.gender_code)?.label || '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
|
|
||||||
<DetailRow label="NIK">{{ person.residentIdentityNumber || '-' }}</DetailRow>
|
<DetailRow label="NIK">{{ patient.person.residentIdentityNumber || '-' }}</DetailRow>
|
||||||
<DetailRow label="No. SIM">{{ person.drivingLicenseNumber || '-' }}</DetailRow>
|
<DetailRow label="No. SIM">{{ patient.person.drivingLicenseNumber || '-' }}</DetailRow>
|
||||||
<DetailRow label="No. Paspor">{{ person.passportNumber || '-' }}</DetailRow>
|
<DetailRow label="No. Paspor">{{ patient.person.passportNumber || '-' }}</DetailRow>
|
||||||
|
|
||||||
<DetailRow label="Agama">
|
<DetailRow label="Agama">
|
||||||
{{ religionOptions.find((item) => item.code === person.religion_code)?.label || '-' }}
|
{{ religionOptions.find((item) => item.code === patient.person.religion_code)?.label || '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Suku">{{ person.ethnic_code || '-' }}</DetailRow>
|
<DetailRow label="Suku">{{ patient.person.ethnic?.name || '-' }}</DetailRow>
|
||||||
<DetailRow label="Bahasa">{{ person.language_code || '-' }}</DetailRow>
|
<DetailRow label="Bahasa">{{ patient.person.language?.name || '-' }}</DetailRow>
|
||||||
<DetailRow label="Pendidikan">
|
<DetailRow label="Pendidikan">
|
||||||
{{ educationOptions.find((item) => item.code === person.education_code)?.label || '-' }}
|
{{ educationOptions.find((item) => item.code === patient.person.education_code)?.label || '-' }}
|
||||||
|
</DetailRow>
|
||||||
|
<DetailRow label="Pekerjaan">
|
||||||
|
{{
|
||||||
|
occupationOptions.find((item) => item.code === patient.person.occupation_code)?.label ||
|
||||||
|
patient.person.occupation_name ||
|
||||||
|
'-'
|
||||||
|
}}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Pekerjaan">{{ person.occupation_name || '-' }}</DetailRow>
|
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
|
|
||||||
<DetailSection title="Alamat">
|
<DetailSection title="Alamat">
|
||||||
<DetailRow label="Alamat Domisili">{{ residentAddress || '-' }}</DetailRow>
|
<DetailRow :label="addressLocationTypeCode.domicile || 'Alamat Domisili'">{{ domicileAddress || '-' }}</DetailRow>
|
||||||
<DetailRow label="Alamat KTP">{{ primaryAddress || '-' }}</DetailRow>
|
<DetailRow :label="addressLocationTypeCode.identity || 'Alamat KTP'">{{ identityAddress || '-' }}</DetailRow>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
<DetailSection title="Kontak">
|
<DetailSection title="Kontak">
|
||||||
<template v-if="personContacts && personContacts.length > 0">
|
<template v-if="patient.person.contacts && patient.person.contacts.length > 0">
|
||||||
<template
|
<template
|
||||||
v-for="contactType in personContactTypeOptions"
|
v-for="contactType in personContactTypeOptions"
|
||||||
:key="contactType.code"
|
:key="contactType.code"
|
||||||
>
|
>
|
||||||
<DetailRow :label="contactType.label">
|
<DetailRow :label="contactType.label">
|
||||||
{{ personContacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
|
{{ patient.person.contacts.find((item) => item.type_code === contactType.code)?.value || '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -116,10 +135,10 @@ function onClick(type: string) {
|
|||||||
<DetailRow label="Kontak">-</DetailRow>
|
<DetailRow label="Kontak">-</DetailRow>
|
||||||
</template>
|
</template>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
<DetailSection title="Penanggung Jawab">
|
<DetailSection title="Orang Tua">
|
||||||
<template v-if="personRelatives && personRelatives.filter((rel) => rel.responsible).length > 0">
|
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => !rel.responsible).length > 0">
|
||||||
<template
|
<template
|
||||||
v-for="(relative, index) in personRelatives.filter((rel) => rel.responsible)"
|
v-for="(relative, index) in patient.person.relatives.filter((rel) => !rel.responsible)"
|
||||||
:key="relative.id"
|
:key="relative.id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -130,13 +149,41 @@ function onClick(type: string) {
|
|||||||
<DetailRow label="Hubungan">
|
<DetailRow label="Hubungan">
|
||||||
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Jenis Kelamin">
|
<!-- <DetailRow label="Jenis Kelamin">
|
||||||
{{ genderOptions.find((item) => item.code === relative.gender_code)?.label || '-' }}
|
{{ genderOptions.find((item) => item.code === relative.gender_code)?.label || '-' }}
|
||||||
</DetailRow>
|
</DetailRow> -->
|
||||||
<DetailRow label="Pendidikan">
|
<DetailRow label="Pendidikan">
|
||||||
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
|
{{ educationOptions.find((item) => item.code === relative.education_code)?.label || '-' }}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Pekerjaan">{{ relative.occupation_name || '-' }}</DetailRow>
|
<DetailRow label="Pekerjaan">
|
||||||
|
{{
|
||||||
|
occupationOptions.find((item) => item.code === relative.occupation_code)?.label ||
|
||||||
|
relative.occupation_name ||
|
||||||
|
'-'
|
||||||
|
}}
|
||||||
|
</DetailRow>
|
||||||
|
<!-- <DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow> -->
|
||||||
|
<!-- <DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow> -->
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DetailRow label="Orang Tua">-</DetailRow>
|
||||||
|
</template>
|
||||||
|
</DetailSection>
|
||||||
|
<DetailSection title="Penanggung Jawab">
|
||||||
|
<template v-if="patient.person.relatives && patient.person.relatives.filter((rel) => rel.responsible).length > 0">
|
||||||
|
<template
|
||||||
|
v-for="(relative, index) in patient.person.relatives.filter((rel) => rel.responsible)"
|
||||||
|
:key="relative.id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="index > 0"
|
||||||
|
class="mt-3 border-t border-gray-200 pt-3"
|
||||||
|
></div>
|
||||||
|
<DetailRow label="Nama">{{ relative.name || '-' }}</DetailRow>
|
||||||
|
<DetailRow label="Hubungan">
|
||||||
|
{{ relationshipOptions.find((item) => item.code === relative.relationship_code)?.label || '-' }}
|
||||||
|
</DetailRow>
|
||||||
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
|
<DetailRow label="Alamat">{{ relative.address || '-' }}</DetailRow>
|
||||||
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
|
<DetailRow label="Nomor HP">{{ relative.phoneNumber || '-' }}</DetailRow>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import Field from '~/components/pub/my-ui/form/field.vue'
|
|||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName?: string
|
||||||
|
regencyCode?: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
@@ -17,24 +20,41 @@ const props = defineProps<{
|
|||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { placeholder = 'Pilih Kecamatan', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
const {
|
||||||
|
fieldName = 'districtId',
|
||||||
|
placeholder = 'Pilih kecamatan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
const districtOptions = [
|
// Gunakan composable untuk mengelola data districts
|
||||||
{ label: 'Kecamatan Lowokwaru', value: '18' },
|
const regencyCodeRef = toRef(props, 'regencyCode')
|
||||||
{ label: 'Kecamatan Pakis', value: '33' },
|
const { districtOptions, isLoading, error } = useDistricts(regencyCodeRef)
|
||||||
{ label: 'Kecamatan Blimbing', value: '35' },
|
|
||||||
]
|
// Computed untuk menentukan placeholder berdasarkan state
|
||||||
|
const dynamicPlaceholder = computed(() => {
|
||||||
|
if (!props.regencyCode) return 'Pilih kabupaten/kota dahulu'
|
||||||
|
if (isLoading.value) return 'Memuat data kecamatan...'
|
||||||
|
if (error.value) return 'Gagal memuat data'
|
||||||
|
return placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan apakah field disabled
|
||||||
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || !props.regencyCode || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
Kecamatan
|
Kecamatan
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -49,15 +69,15 @@ const districtOptions = [
|
|||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="districtOptions"
|
:items="districtOptions"
|
||||||
:placeholder="placeholder"
|
:placeholder="dynamicPlaceholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isFieldDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari kecamatan..."
|
||||||
empty-message="Kecamatan tidak ditemukan"
|
empty-message="Kecamatan tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,8 +6,11 @@ import Field from '~/components/pub/my-ui/form/field.vue'
|
|||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName: string
|
||||||
|
villageCode?: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
@@ -17,37 +20,32 @@ const props = defineProps<{
|
|||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const {
|
const { fieldName = 'postalRegion', placeholder = 'Kode Pos', errors, class: containerClass, fieldGroupClass } = props
|
||||||
fieldName = 'zipCode',
|
|
||||||
placeholder = 'Kode Pos',
|
|
||||||
errors,
|
|
||||||
class: containerClass,
|
|
||||||
selectClass,
|
|
||||||
fieldGroupClass,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const postalCodeOptions = [
|
const villageCodeRef = toRef(props, 'villageCode')
|
||||||
{ label: '65120', value: '65120' },
|
const { postalRegionOptions, isLoading, error } = usePostalRegion(villageCodeRef)
|
||||||
{ label: '65121', value: '65121' },
|
|
||||||
{ label: '65123', value: '65123' },
|
const dynamicPlaceholder = computed(() => {
|
||||||
{ label: '65124', value: '65124' },
|
if (!props.villageCode) return 'Pilih kelurahan terlebih dahulu'
|
||||||
{ label: '65125', value: '65125' },
|
if (isLoading.value) return 'Memuat kode pos...'
|
||||||
{ label: '65126', value: '65126' },
|
if (error.value) return 'Gagal memuat data'
|
||||||
{ label: '65127', value: '65127' },
|
return placeholder
|
||||||
{ label: '65128', value: '65128' },
|
})
|
||||||
{ label: '65129', value: '65129' },
|
|
||||||
]
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || !props.villageCode || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
Kode Pos
|
Kode Pos
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -61,9 +59,9 @@ const postalCodeOptions = [
|
|||||||
<Combobox
|
<Combobox
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="postalCodeOptions"
|
:items="postalRegionOptions"
|
||||||
:placeholder="placeholder"
|
:placeholder="dynamicPlaceholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isFieldDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari..."
|
||||||
empty-message="Kode pos tidak ditemukan"
|
empty-message="Kode pos tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
@@ -71,6 +69,6 @@ const postalCodeOptions = [
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,8 +6,10 @@ import Field from '~/components/pub/my-ui/form/field.vue'
|
|||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName?: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
@@ -18,29 +20,38 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fieldName = 'provinceId',
|
fieldName = 'provinceCode',
|
||||||
placeholder = 'Pilih provinsi',
|
placeholder = 'Pilih provinsi',
|
||||||
errors,
|
errors,
|
||||||
class: containerClass,
|
class: containerClass,
|
||||||
fieldGroupClass,
|
fieldGroupClass,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const provinceList = [
|
// Gunakan composable untuk mengelola data provinces
|
||||||
{ label: 'Jawa Barat', value: '18' },
|
const { provinceOptions, isLoading, error } = useProvinces()
|
||||||
{ label: 'Jawa Tengah', value: '33' },
|
|
||||||
{ label: 'Jawa Timur', value: '35' },
|
// Computed untuk menentukan placeholder berdasarkan state
|
||||||
]
|
const dynamicPlaceholder = computed(() => {
|
||||||
|
if (isLoading.value) return 'Memuat data provinsi...'
|
||||||
|
if (error.value) return 'Gagal memuat data'
|
||||||
|
return placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan apakah field disabled
|
||||||
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
Provinsi
|
Provinsi
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -54,16 +65,16 @@ const provinceList = [
|
|||||||
<Combobox
|
<Combobox
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="provinceList"
|
:items="provinceOptions"
|
||||||
:placeholder="placeholder"
|
:placeholder="dynamicPlaceholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isFieldDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari provinsi..."
|
||||||
empty-message="Kecamatan tidak ditemukan"
|
empty-message="Provinsi tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,8 +6,11 @@ import Field from '~/components/pub/my-ui/form/field.vue'
|
|||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName?: string
|
||||||
|
provinceCode?: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
@@ -17,26 +20,41 @@ const props = defineProps<{
|
|||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { placeholder = 'Pilih kabupaten/kota', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
const {
|
||||||
|
fieldName = 'regencyId',
|
||||||
|
placeholder = 'Pilih kabupaten/kota',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
const regencyOptions = [
|
// Gunakan composable untuk mengelola data regencies
|
||||||
{ label: 'Kab. Sidoarjo', value: '32' },
|
const provinceCodeRef = toRef(props, 'provinceCode')
|
||||||
{ label: 'Kab. Malang', value: '35' },
|
const { regencyOptions, isLoading, error } = useRegencies({ provinceCode: provinceCodeRef, enablePagination: false })
|
||||||
{ label: 'Kab. Mojokerto', value: '31' },
|
|
||||||
{ label: 'Kab. Lamongan', value: '30' },
|
// Computed untuk menentukan placeholder berdasarkan state
|
||||||
{ label: 'Kota Malang', value: '18' },
|
const dynamicPlaceholder = computed(() => {
|
||||||
]
|
if (!props.provinceCode) return 'Pilih provinsi dahulu'
|
||||||
|
if (isLoading.value) return 'Memuat data kabupaten/kota...'
|
||||||
|
if (error.value) return 'Gagal memuat data'
|
||||||
|
return placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan apakah field disabled
|
||||||
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || !props.provinceCode || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
Kabupaten/Kota
|
Kabupaten/Kota
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -51,15 +69,15 @@ const regencyOptions = [
|
|||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="regencyOptions"
|
:items="regencyOptions"
|
||||||
:placeholder="placeholder"
|
:placeholder="dynamicPlaceholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isFieldDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari kabupaten/kota..."
|
||||||
empty-message="Kabupaten/kota tidak ditemukan"
|
empty-message="Kabupaten/kota tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,8 +6,11 @@ import Field from '~/components/pub/my-ui/form/field.vue'
|
|||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName?: string
|
||||||
|
districtCode?: string
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
@@ -17,25 +20,41 @@ const props = defineProps<{
|
|||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { placeholder = 'Pilih Kelurahan', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
const {
|
||||||
|
fieldName = 'villageId',
|
||||||
|
placeholder = 'Pilih kelurahan',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
const villageOptions = [
|
// Gunakan composable untuk mengelola data villages
|
||||||
{ label: 'Lowokwaru', value: '18' },
|
const districtCodeRef = toRef(props, 'districtCode')
|
||||||
{ label: 'Dinoyo', value: '33' },
|
const { villageOptions, isLoading, error } = useVillages(districtCodeRef)
|
||||||
{ label: 'Blimbing', value: '35' },
|
|
||||||
{ label: 'Sawojajar', value: '36' },
|
// Computed untuk menentukan placeholder berdasarkan state
|
||||||
]
|
const dynamicPlaceholder = computed(() => {
|
||||||
|
if (!props.districtCode) return 'Pilih kecamatan dahulu'
|
||||||
|
if (isLoading.value) return 'Memuat data kelurahan...'
|
||||||
|
if (error.value) return 'Gagal memuat data'
|
||||||
|
return placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan apakah field disabled
|
||||||
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || !props.districtCode || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
<Label
|
<DE.Label
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired"
|
:is-required="isRequired"
|
||||||
>
|
>
|
||||||
Kelurahan
|
Kelurahan
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:class="cn('select-field-wrapper')"
|
:class="cn('select-field-wrapper')"
|
||||||
@@ -50,15 +69,15 @@ const villageOptions = [
|
|||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="villageOptions"
|
:items="villageOptions"
|
||||||
:placeholder="placeholder"
|
:placeholder="dynamicPlaceholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isFieldDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari kelurahan..."
|
||||||
empty-message="Kelurahan tidak ditemukan"
|
empty-message="Kelurahan tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,7 +6,8 @@ import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
|
||||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
import RadioResidence from './_common/radio-residence.vue'
|
import { Label as RadioLabel } from '~/components/pub/ui/label'
|
||||||
|
import { RadioGroup, RadioGroupItem } from '~/components/pub/ui/radio-group'
|
||||||
import SelectDistrict from './_common/select-district.vue'
|
import SelectDistrict from './_common/select-district.vue'
|
||||||
import SelectPostal from './_common/select-postal.vue'
|
import SelectPostal from './_common/select-postal.vue'
|
||||||
import SelectProvince from './_common/select-province.vue'
|
import SelectProvince from './_common/select-province.vue'
|
||||||
@@ -14,6 +15,8 @@ import SelectRegency from './_common/select-regency.vue'
|
|||||||
import SelectVillage from './_common/select-village.vue'
|
import SelectVillage from './_common/select-village.vue'
|
||||||
import { Form } from '~/components/pub/ui/form'
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
conf?: {
|
conf?: {
|
||||||
@@ -39,18 +42,27 @@ let isResetting = false
|
|||||||
|
|
||||||
// Field dependency map for placeholder
|
// Field dependency map for placeholder
|
||||||
const fieldStates: Record<string, { dependsOn?: string; placeholder: string }> = {
|
const fieldStates: Record<string, { dependsOn?: string; placeholder: string }> = {
|
||||||
regencyId: { dependsOn: 'provinceId', placeholder: 'Pilih provinsi dahulu' },
|
regency_code: { dependsOn: 'province_code', placeholder: 'Pilih provinsi dahulu' },
|
||||||
districtId: { dependsOn: 'regencyId', placeholder: 'Pilih kabupaten/kota dahulu' },
|
district_code: { dependsOn: 'regency_code', placeholder: 'Pilih kabupaten/kota dahulu' },
|
||||||
villageId: { dependsOn: 'districtId', placeholder: 'Pilih kecamatan dahulu' },
|
village_code: { dependsOn: 'district_code', placeholder: 'Pilih kecamatan dahulu' },
|
||||||
zipCode: { dependsOn: 'villageId', placeholder: 'Pilih kelurahan dahulu' },
|
postalRegion_code: { dependsOn: 'village_code', placeholder: 'Pilih kelurahan dahulu' },
|
||||||
address: { placeholder: 'Masukkan alamat' },
|
address: { placeholder: 'Masukkan alamat' },
|
||||||
rt: { placeholder: '001' },
|
rt: { placeholder: '001' },
|
||||||
rw: { placeholder: '002' },
|
rw: { placeholder: '002' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computed untuk konversi boolean ke string untuk radio group
|
||||||
|
const isSameAddressString = computed(() => {
|
||||||
|
const value = formRef.value?.values?.isSameAddress
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? '1' : '0'
|
||||||
|
}
|
||||||
|
return value || '1'
|
||||||
|
})
|
||||||
// #region Function Helper
|
// #region Function Helper
|
||||||
function getFieldState(field: string) {
|
function getFieldState(field: string) {
|
||||||
const state = fieldStates[field]
|
const state = fieldStates[field]
|
||||||
const isSame = formRef.value?.values?.isSameAddress === '1'
|
const isSame = formRef.value?.values?.isSameAddress === true || formRef.value?.values?.isSameAddress === '1'
|
||||||
|
|
||||||
// Jika alamat sama, semua field kecuali provinsi disabled
|
// Jika alamat sama, semua field kecuali provinsi disabled
|
||||||
if (['address', 'rt', 'rw'].includes(field) && isSame) {
|
if (['address', 'rt', 'rw'].includes(field) && isSame) {
|
||||||
@@ -63,7 +75,7 @@ function getFieldState(field: string) {
|
|||||||
const isDisabledByDependency = !dependencyValue
|
const isDisabledByDependency = !dependencyValue
|
||||||
|
|
||||||
// Jika isSame, semua field location disabled
|
// Jika isSame, semua field location disabled
|
||||||
if (isSame && ['regencyId', 'districtId', 'villageId', 'zipCode'].includes(field)) {
|
if (isSame && ['regency_code', 'district_code', 'village_code', 'postalRegion_code'].includes(field)) {
|
||||||
return { placeholder: '-', disabled: true }
|
return { placeholder: '-', disabled: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +85,7 @@ function getFieldState(field: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Jika isSame dan field location, disabled
|
// Jika isSame dan field location, disabled
|
||||||
if (isSame && ['regencyId', 'districtId', 'villageId', 'zipCode'].includes(field)) {
|
if (isSame && ['regency_code', 'district_code', 'village_code', 'postalRegion_code'].includes(field)) {
|
||||||
return { placeholder: '-', disabled: true }
|
return { placeholder: '-', disabled: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +96,9 @@ function getFieldState(field: string) {
|
|||||||
|
|
||||||
// #region watch
|
// #region watch
|
||||||
|
|
||||||
// Watch provinceId changes
|
// Watch province_code changes
|
||||||
watch(
|
watch(
|
||||||
() => formRef.value?.values?.provinceId,
|
() => formRef.value?.values?.province_code,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
@@ -95,10 +107,10 @@ watch(
|
|||||||
|
|
||||||
formRef.value.setValues(
|
formRef.value.setValues(
|
||||||
{
|
{
|
||||||
regencyId: undefined,
|
regency_code: undefined,
|
||||||
districtId: undefined,
|
district_code: undefined,
|
||||||
villageId: undefined,
|
village_code: undefined,
|
||||||
zipCode: undefined,
|
postalRegion_code: undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -110,9 +122,9 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watch regencyId changes
|
// Watch regency_code changes
|
||||||
watch(
|
watch(
|
||||||
() => formRef.value?.values?.regencyId,
|
() => formRef.value?.values?.regency_code,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
@@ -121,9 +133,9 @@ watch(
|
|||||||
|
|
||||||
formRef.value.setValues(
|
formRef.value.setValues(
|
||||||
{
|
{
|
||||||
districtId: undefined,
|
district_code: undefined,
|
||||||
villageId: undefined,
|
village_code: undefined,
|
||||||
zipCode: undefined,
|
postalRegion_code: undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -135,9 +147,9 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watch districtId changes
|
// Watch district_code changes
|
||||||
watch(
|
watch(
|
||||||
() => formRef.value?.values?.districtId,
|
() => formRef.value?.values?.district_code,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
@@ -146,8 +158,8 @@ watch(
|
|||||||
|
|
||||||
formRef.value.setValues(
|
formRef.value.setValues(
|
||||||
{
|
{
|
||||||
villageId: undefined,
|
village_code: undefined,
|
||||||
zipCode: undefined,
|
postalRegion_code: undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -159,9 +171,9 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watch villageId changes
|
// Watch village_code changes
|
||||||
watch(
|
watch(
|
||||||
() => formRef.value?.values?.villageId,
|
() => formRef.value?.values?.village_code,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
@@ -170,7 +182,7 @@ watch(
|
|||||||
|
|
||||||
formRef.value.setValues(
|
formRef.value.setValues(
|
||||||
{
|
{
|
||||||
zipCode: undefined,
|
postalRegion_code: undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -188,19 +200,23 @@ watch(
|
|||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (!formRef.value || newValue === oldValue) return
|
if (!formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
// Ketika berubah dari '1' ke '0', clear empty strings dan trigger validasi
|
// Konversi ke boolean untuk perbandingan yang konsisten
|
||||||
if (oldValue === '1' && newValue === '0') {
|
const newBool = newValue === true || newValue === '1'
|
||||||
|
const oldBool = oldValue === true || oldValue === '1'
|
||||||
|
|
||||||
|
// Ketika berubah dari true ke false, clear empty strings dan trigger validasi
|
||||||
|
if (oldBool && !newBool) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// Set empty strings ke undefined untuk trigger required validation
|
// Set empty strings ke undefined untuk trigger required validation
|
||||||
const currentValues = formRef.value.values
|
const currentValues = formRef.value.values
|
||||||
const updatedValues = { ...currentValues }
|
const updatedValues = { ...currentValues }
|
||||||
|
|
||||||
// Convert empty strings to undefined untuk field yang sekarang required
|
// Convert empty strings to undefined untuk field yang sekarang required
|
||||||
if (updatedValues.provinceId === '') updatedValues.provinceId = undefined
|
if (updatedValues.province_code === '') updatedValues.province_code = undefined
|
||||||
if (updatedValues.regencyId === '') updatedValues.regencyId = undefined
|
if (updatedValues.regency_code === '') updatedValues.regency_code = undefined
|
||||||
if (updatedValues.districtId === '') updatedValues.districtId = undefined
|
if (updatedValues.district_code === '') updatedValues.district_code = undefined
|
||||||
if (updatedValues.villageId === '') updatedValues.villageId = undefined
|
if (updatedValues.village_code === '') updatedValues.village_code = undefined
|
||||||
if (updatedValues.zipCode === '') updatedValues.zipCode = undefined
|
if (updatedValues.postalRegion_code === '') updatedValues.postalRegion_code = undefined
|
||||||
if (updatedValues.address === '') updatedValues.address = undefined
|
if (updatedValues.address === '') updatedValues.address = undefined
|
||||||
|
|
||||||
// Update values dan trigger validasi
|
// Update values dan trigger validasi
|
||||||
@@ -213,15 +229,15 @@ watch(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ketika berubah dari '0' ke '1', clear error messages
|
// Ketika berubah dari false ke true, clear error messages
|
||||||
if (oldValue === '0' && newValue === '1') {
|
if (!oldBool && newBool) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// Clear error messages untuk field yang tidak lagi required
|
// Clear error messages untuk field yang tidak lagi required
|
||||||
formRef.value?.setFieldError('provinceId', undefined)
|
formRef.value?.setFieldError('province_code', undefined)
|
||||||
formRef.value?.setFieldError('regencyId', undefined)
|
formRef.value?.setFieldError('regency_code', undefined)
|
||||||
formRef.value?.setFieldError('districtId', undefined)
|
formRef.value?.setFieldError('district_code', undefined)
|
||||||
formRef.value?.setFieldError('villageId', undefined)
|
formRef.value?.setFieldError('village_code', undefined)
|
||||||
formRef.value?.setFieldError('zipCode', undefined)
|
formRef.value?.setFieldError('postalRegion_code', undefined)
|
||||||
formRef.value?.setFieldError('address', undefined)
|
formRef.value?.setFieldError('address', undefined)
|
||||||
formRef.value?.setFieldError('rt', undefined)
|
formRef.value?.setFieldError('rt', undefined)
|
||||||
formRef.value?.setFieldError('rw', undefined)
|
formRef.value?.setFieldError('rw', undefined)
|
||||||
@@ -241,129 +257,138 @@ watch(
|
|||||||
:validation-schema="formSchema"
|
:validation-schema="formSchema"
|
||||||
:validate-on-mount="false"
|
:validate-on-mount="false"
|
||||||
validation-mode="onSubmit"
|
validation-mode="onSubmit"
|
||||||
:initial-values="initialValues ?? { isSameAddress: '1' }"
|
:initial-values="initialValues ?? { isSameAddress: '1', locationType_code: 'identity' }"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
v-if="props.title"
|
v-if="props.title"
|
||||||
class="text-md mb-2 mt-1 font-semibold"
|
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||||
>
|
>
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
<!-- locationType_code - Hidden field with default value 'identity' -->
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2">
|
<FormField
|
||||||
<!-- LocationType -->
|
v-slot="{ componentField }"
|
||||||
<FieldGroup v-if="conf?.withAddressName">
|
name="locationType_code"
|
||||||
<Label label-for="locationType">Jenis Alamat</Label>
|
>
|
||||||
<Field
|
<input
|
||||||
id="locationType"
|
type="hidden"
|
||||||
:errors="errors"
|
v-bind="componentField"
|
||||||
>
|
value="identity"
|
||||||
<FormField
|
/>
|
||||||
v-slot="{ componentField }"
|
</FormField>
|
||||||
name="locationType"
|
<DE.Block :col-count="4" :cell-flex="false">
|
||||||
>
|
<DE.Cell :col-span="4">
|
||||||
<FormItem>
|
<DE.Label
|
||||||
<FormControl>
|
size="fit"
|
||||||
<Select
|
height="compact"
|
||||||
id="locationType"
|
label-for="isSameAddress"
|
||||||
v-bind="componentField"
|
>
|
||||||
:items="[
|
Apakah alamat KTP sama dengan alamat sekarang?
|
||||||
{ label: 'Rumah', value: 'rumah' },
|
</DE.Label>
|
||||||
{ label: 'Kantor', value: 'kantor' },
|
<DE.Field
|
||||||
{ label: 'Lainnya', value: 'lainnya' },
|
id="isSameAddress"
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
<RadioResidence field-name="isSameAddress" />
|
|
||||||
<Block></Block>
|
|
||||||
|
|
||||||
<div class="flex-row gap-2 md:flex">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectProvince
|
|
||||||
field-name="provinceId"
|
|
||||||
placeholder="Pilih"
|
|
||||||
:is-disabled="values.isSameAddress === '1'"
|
|
||||||
:is-required="values.isSameAddress !== '1'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectRegency
|
|
||||||
field-name="regencyId"
|
|
||||||
:placeholder="getFieldState('regencyId').placeholder"
|
|
||||||
:is-disabled="getFieldState('regencyId').disabled || !values.provinceId"
|
|
||||||
:is-required="values.isSameAddress !== '1'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-row gap-2 md:flex">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectDistrict
|
|
||||||
field-name="districtId"
|
|
||||||
:placeholder="getFieldState('districtId').placeholder"
|
|
||||||
:is-disabled="getFieldState('districtId').disabled || !values.regencyId"
|
|
||||||
:is-required="values.isSameAddress !== '1'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectVillage
|
|
||||||
field-name="villageId"
|
|
||||||
:placeholder="getFieldState('villageId').placeholder"
|
|
||||||
:is-disabled="getFieldState('villageId').disabled || !values.districtId"
|
|
||||||
:is-required="values.isSameAddress !== '1'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<InputBase
|
|
||||||
field-name="address"
|
|
||||||
label="Alamat"
|
|
||||||
:placeholder="getFieldState('address').placeholder"
|
|
||||||
:is-disabled="getFieldState('address').disabled"
|
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:is-required="values.isSameAddress !== '1'"
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField }"
|
||||||
|
name="isSameAddress"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
:model-value="isSameAddressString"
|
||||||
|
@update:model-value="(value) => componentField.onChange(value)"
|
||||||
|
class="flex flex-row flex-wrap gap-4 sm:gap-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in [
|
||||||
|
{ label: 'Ya', value: '1' },
|
||||||
|
{ label: 'Tidak', value: '0' },
|
||||||
|
]"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex min-w-fit items-center space-x-2"
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
:id="`isSameAddress-${index}`"
|
||||||
|
:value="option.value"
|
||||||
|
class="relative h-4 w-4 rounded-full border-2 border-muted-foreground before:absolute before:inset-1 before:rounded-full before:bg-primary before:opacity-0 before:transition-opacity data-[state=checked]:border-primary data-[state=checked]:bg-white data-[state=checked]:before:opacity-100 sm:h-5 sm:w-5"
|
||||||
|
/>
|
||||||
|
<RadioLabel
|
||||||
|
:for="`isSameAddress-${index}`"
|
||||||
|
class="cursor-pointer select-none text-xs font-medium leading-none transition-colors hover:text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</RadioLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage class="ml-0 mt-1" />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
<SelectProvince
|
||||||
|
field-name="province_code"
|
||||||
|
placeholder="Pilih"
|
||||||
|
:is-disabled="values.isSameAddress === true || values.isSameAddress === '1'"
|
||||||
|
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||||
|
/>
|
||||||
|
<SelectRegency
|
||||||
|
field-name="regency_code"
|
||||||
|
:province-code="values.province_code"
|
||||||
|
:is-disabled="getFieldState('regency_code').disabled"
|
||||||
|
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||||
|
/>
|
||||||
|
<SelectDistrict
|
||||||
|
field-name="district_code"
|
||||||
|
:regency-code="values.regency_code"
|
||||||
|
:is-disabled="getFieldState('district_code').disabled"
|
||||||
|
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||||
|
/>
|
||||||
|
<SelectVillage
|
||||||
|
field-name="village_code"
|
||||||
|
:district-code="values.district_code"
|
||||||
|
:is-disabled="getFieldState('village_code').disabled"
|
||||||
|
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="address"
|
||||||
|
label="Alamat"
|
||||||
|
:placeholder="getFieldState('address').placeholder"
|
||||||
|
:is-disabled="getFieldState('address').disabled"
|
||||||
|
:errors="errors"
|
||||||
|
:is-required="values.isSameAddress !== true && values.isSameAddress !== '1'"
|
||||||
|
:col-span="2"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-1">
|
||||||
|
<InputBase
|
||||||
|
field-name="rt"
|
||||||
|
label="RT"
|
||||||
|
:errors="errors"
|
||||||
|
numeric-only
|
||||||
|
:max-length="2"
|
||||||
|
:placeholder="getFieldState('rt').placeholder"
|
||||||
|
:is-disabled="getFieldState('rt').disabled"
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="rw"
|
||||||
|
label="RW"
|
||||||
|
:placeholder="getFieldState('rw').placeholder"
|
||||||
|
:is-disabled="getFieldState('rw').disabled"
|
||||||
|
:errors="errors"
|
||||||
|
:max-length="2"
|
||||||
|
numeric-only
|
||||||
/>
|
/>
|
||||||
<div class="flex-row gap-2 md:flex">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<InputBase
|
|
||||||
field-name="rt"
|
|
||||||
label="RT"
|
|
||||||
:errors="errors"
|
|
||||||
numeric-only
|
|
||||||
:max-length="3"
|
|
||||||
:placeholder="getFieldState('rt').placeholder"
|
|
||||||
:is-disabled="getFieldState('rt').disabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<InputBase
|
|
||||||
field-name="rw"
|
|
||||||
label="RW"
|
|
||||||
:placeholder="getFieldState('rw').placeholder"
|
|
||||||
:is-disabled="getFieldState('rw').disabled"
|
|
||||||
:errors="errors"
|
|
||||||
:max-length="3"
|
|
||||||
numeric-only
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-[2]">
|
|
||||||
<SelectPostal
|
|
||||||
field-name="zipCode"
|
|
||||||
:placeholder="getFieldState('zipCode').placeholder"
|
|
||||||
:is-disabled="getFieldState('zipCode').disabled || !values.villageId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<SelectPostal
|
||||||
|
field-name="postalRegion_code"
|
||||||
|
:village-code="values.village_code"
|
||||||
|
:placeholder="getFieldState('postalRegion_code').placeholder"
|
||||||
|
:is-disabled="getFieldState('postalRegion_code').disabled || !values.village_code"
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
@@ -12,6 +12,8 @@ import SelectRegency from './_common/select-regency.vue'
|
|||||||
import SelectVillage from './_common/select-village.vue'
|
import SelectVillage from './_common/select-village.vue'
|
||||||
import { Form } from '~/components/pub/ui/form'
|
import { Form } from '~/components/pub/ui/form'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
conf?: {
|
conf?: {
|
||||||
@@ -35,29 +37,34 @@ defineExpose({
|
|||||||
// Watchers untuk cascading reset
|
// Watchers untuk cascading reset
|
||||||
let isResetting = false
|
let isResetting = false
|
||||||
|
|
||||||
// #region Watch provinceId changes
|
// #region Watch provinceCode changes
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => formRef.value?.values?.provinceId,
|
() => formRef.value?.values?.provinceCode,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (isResetting || !formRef.value || newValue === oldValue) return
|
if (isResetting || !formRef.value || newValue === oldValue) return
|
||||||
|
|
||||||
if (oldValue && newValue !== oldValue) {
|
if (oldValue && newValue !== oldValue) {
|
||||||
isResetting = true
|
isResetting = true
|
||||||
|
|
||||||
formRef.value.setValues(
|
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||||
{
|
setTimeout(() => {
|
||||||
regencyId: undefined,
|
if (formRef.value) {
|
||||||
districtId: undefined,
|
formRef.value.setValues(
|
||||||
villageId: undefined,
|
{
|
||||||
zipCode: undefined,
|
regencyId: undefined,
|
||||||
},
|
districtId: undefined,
|
||||||
false,
|
villageId: undefined,
|
||||||
)
|
zipCode: undefined,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
isResetting = false
|
isResetting = false
|
||||||
})
|
})
|
||||||
|
}, 150) // Delay 150ms, lebih dari debounce composable (100ms)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -71,18 +78,23 @@ watch(
|
|||||||
if (oldValue && newValue !== oldValue) {
|
if (oldValue && newValue !== oldValue) {
|
||||||
isResetting = true
|
isResetting = true
|
||||||
|
|
||||||
formRef.value.setValues(
|
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||||
{
|
setTimeout(() => {
|
||||||
districtId: undefined,
|
if (formRef.value) {
|
||||||
villageId: undefined,
|
formRef.value.setValues(
|
||||||
zipCode: undefined,
|
{
|
||||||
},
|
districtId: undefined,
|
||||||
false,
|
villageId: undefined,
|
||||||
)
|
zipCode: undefined,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
isResetting = false
|
isResetting = false
|
||||||
})
|
})
|
||||||
|
}, 150)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -96,17 +108,22 @@ watch(
|
|||||||
if (oldValue && newValue !== oldValue) {
|
if (oldValue && newValue !== oldValue) {
|
||||||
isResetting = true
|
isResetting = true
|
||||||
|
|
||||||
formRef.value.setValues(
|
// Delay reset untuk memberikan waktu composable menyelesaikan request
|
||||||
{
|
setTimeout(() => {
|
||||||
villageId: undefined,
|
if (formRef.value) {
|
||||||
zipCode: undefined,
|
formRef.value.setValues(
|
||||||
},
|
{
|
||||||
false,
|
villageId: undefined,
|
||||||
)
|
zipCode: undefined,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
isResetting = false
|
isResetting = false
|
||||||
})
|
})
|
||||||
|
}, 150)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -145,124 +162,84 @@ watch(
|
|||||||
:validation-schema="formSchema"
|
:validation-schema="formSchema"
|
||||||
:validate-on-mount="false"
|
:validate-on-mount="false"
|
||||||
validation-mode="onSubmit"
|
validation-mode="onSubmit"
|
||||||
:initial-values="initialValues ? initialValues : {}"
|
:initial-values="
|
||||||
|
initialValues ? { locationType_code: 'domicile', ...initialValues } : { locationType_code: 'domicile' }
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
v-if="props.title"
|
v-if="props.title"
|
||||||
class="text-md mb-2 mt-1 font-semibold"
|
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||||
>
|
>
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
<FormField
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2">
|
v-slot="{ componentField }"
|
||||||
<!-- LocationType -->
|
name="locationType_code"
|
||||||
<FieldGroup v-if="conf?.withAddressName">
|
>
|
||||||
<Label label-for="locationType">Jenis Alamat</Label>
|
<input
|
||||||
<Field
|
type="hidden"
|
||||||
id="locationType"
|
v-bind="componentField"
|
||||||
|
value="domicile"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<DE.Block :col-count="4" :cell-flex="false">
|
||||||
|
<SelectProvince
|
||||||
|
field-name="province_code"
|
||||||
|
placeholder="Pilih"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectRegency
|
||||||
|
field-name="regency_code"
|
||||||
|
:province-code="values.province_code"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDistrict
|
||||||
|
field-name="district_code"
|
||||||
|
:regency-code="values.regency_code"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectVillage
|
||||||
|
field-name="village_code"
|
||||||
|
:district-code="values.district_code"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<InputBase
|
||||||
|
field-name="address"
|
||||||
|
label="Alamat"
|
||||||
|
placeholder="Masukkan alamat"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
:col-span="2"
|
||||||
|
/>
|
||||||
|
<DE.Cell class="flex-row gap-2">
|
||||||
|
<div class="grid grid-cols-2 gap-1">
|
||||||
|
<InputBase
|
||||||
|
field-name="rt"
|
||||||
|
label="RT"
|
||||||
|
placeholder="01"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
>
|
numeric-only
|
||||||
<FormField
|
:max-length="2"
|
||||||
v-slot="{ componentField }"
|
|
||||||
name="locationType"
|
|
||||||
>
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
id="locationType"
|
|
||||||
v-bind="componentField"
|
|
||||||
:items="[
|
|
||||||
{ label: 'Rumah', value: 'rumah' },
|
|
||||||
{ label: 'Kantor', value: 'kantor' },
|
|
||||||
{ label: 'Lainnya', value: 'lainnya' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
|
|
||||||
<div class="flex-row gap-2 md:flex">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectProvince
|
|
||||||
field-name="provinceId"
|
|
||||||
placeholder="Pilih"
|
|
||||||
is-required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<InputBase
|
||||||
|
field-name="rw"
|
||||||
<div class="min-w-0 flex-1">
|
label="RW"
|
||||||
<SelectRegency
|
placeholder="02"
|
||||||
field-name="regencyId"
|
:errors="errors"
|
||||||
placeholder="Pilih provinsi dahulu"
|
:max-length="2"
|
||||||
:is-disabled="!values.provinceId"
|
numeric-only
|
||||||
is-required
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</DE.Cell>
|
||||||
<div class="flex-row gap-2 md:flex">
|
<SelectPostal
|
||||||
<div class="min-w-0 flex-1">
|
field-name="postalRegion_code"
|
||||||
<SelectDistrict
|
placeholder="Pilih kelurahan dahulu"
|
||||||
field-name="districtId"
|
:village-code="values.village_code"
|
||||||
placeholder="Pilih kabupaten/kota dahulu"
|
:is-disabled="!values.village_code"
|
||||||
:is-disabled="!values.regencyId"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<SelectVillage
|
|
||||||
field-name="villageId"
|
|
||||||
placeholder="Pilih kecamatan dahulu"
|
|
||||||
:is-disabled="!values.districtId"
|
|
||||||
is-required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<InputBase
|
|
||||||
field-name="address"
|
|
||||||
label="Alamat"
|
|
||||||
placeholder="Masukkan alamat"
|
|
||||||
:errors="errors"
|
|
||||||
is-required
|
|
||||||
/>
|
/>
|
||||||
<div class="flex-row gap-2 md:flex">
|
</DE.Block>
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<InputBase
|
|
||||||
field-name="rt"
|
|
||||||
label="RT"
|
|
||||||
placeholder="001"
|
|
||||||
:errors="errors"
|
|
||||||
numeric-only
|
|
||||||
:max-length="3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<InputBase
|
|
||||||
field-name="rw"
|
|
||||||
label="RW"
|
|
||||||
placeholder="002"
|
|
||||||
:errors="errors"
|
|
||||||
:max-length="3"
|
|
||||||
numeric-only
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-[2]">
|
|
||||||
<SelectPostal
|
|
||||||
field-name="zipCode"
|
|
||||||
placeholder="Pilih kelurahan dahulu"
|
|
||||||
:is-disabled="!values.villageId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,7 +39,7 @@ defineExpose({
|
|||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
v-if="props.title"
|
v-if="props.title"
|
||||||
class="text-md mb-2 mt-1 font-semibold"
|
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||||
>
|
>
|
||||||
{{ props.title || 'Kontak Pasien' }}
|
{{ props.title || 'Kontak Pasien' }}
|
||||||
</p>
|
</p>
|
||||||
@@ -94,6 +94,7 @@ defineExpose({
|
|||||||
:disabled="fields.length >= contactLimit"
|
:disabled="fields.length >= contactLimit"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
@click="push({ contactType: '', contactNumber: '' })"
|
@click="push({ contactType: '', contactNumber: '' })"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
|||||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
import Select from '~/components/pub/my-ui/form/select.vue'
|
import Select from '~/components/pub/my-ui/form/select.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
import { relationshipCodes } from '~/lib/constants'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName: string
|
||||||
@@ -18,15 +19,11 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const { fieldName = 'phoneNumber', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
const { fieldName = 'phoneNumber', errors, class: containerClass, selectClass, fieldGroupClass } = props
|
||||||
|
|
||||||
const emergencyContactOptions = [
|
const emergencyContactOptions = Object.entries(relationshipCodes).map(([value, label]) => ({
|
||||||
{ label: 'Diri sendiri', value: 'self' },
|
label,
|
||||||
{ label: 'Orang Tua', value: 'parent' },
|
value,
|
||||||
{ label: 'Anak', value: 'child' },
|
...(value === 'other' && { priority: -1 })
|
||||||
{ label: 'Keluarga lain', value: 'relative' },
|
}))
|
||||||
{ label: 'Petugas instansi lainnya', value: 'institution_officer' },
|
|
||||||
{ label: 'Petugas kesehatan', value: 'health_officer' },
|
|
||||||
{ label: 'Lainnya', value: 'other', priority: -1 },
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName?: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
selectClass?: string
|
||||||
|
fieldGroupClass?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldName = 'birthPlace',
|
||||||
|
placeholder = 'Pilih tempat lahir',
|
||||||
|
errors,
|
||||||
|
class: containerClass,
|
||||||
|
fieldGroupClass,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// Gunakan composable untuk mengelola data regencies (tanpa pagination & tanpa province code)
|
||||||
|
const { fetchRegencies, regencyOptions, isLoading, error } = useRegencies({
|
||||||
|
enablePagination: false,
|
||||||
|
enableSearch: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan placeholder berdasarkan state
|
||||||
|
const dynamicPlaceholder = computed(() => {
|
||||||
|
if (isLoading.value) return 'Memuat data tempat lahir...'
|
||||||
|
if (error.value) return 'Gagal memuat data'
|
||||||
|
return placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk menentukan apakah field disabled
|
||||||
|
const isFieldDisabled = computed(() => {
|
||||||
|
return props.isDisabled || isLoading.value || !!error.value
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchRegencies()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
|
||||||
|
<DE.Label :label-for="fieldName" :is-required="isRequired">
|
||||||
|
Tempat Lahir
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field :id="fieldName" :errors="errors" :class="cn('select-field-wrapper')">
|
||||||
|
<FormField v-slot="{ componentField }" :name="fieldName">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
:id="fieldName"
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="regencyOptions"
|
||||||
|
:placeholder="dynamicPlaceholder"
|
||||||
|
:is-disabled="isFieldDisabled"
|
||||||
|
search-placeholder="Cari tempat lahir..."
|
||||||
|
empty-message="Tempat lahir tidak ditemukan"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -65,7 +65,7 @@ defineExpose({
|
|||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
v-if="props.title"
|
v-if="props.title"
|
||||||
class="text-md mb-2 mt-1 font-semibold"
|
class="text-sm 2xl:text-base mb-2 2xl:mb-3 font-semibold"
|
||||||
>
|
>
|
||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
|
||||||
|
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
|
||||||
|
import Button from '~/components/pub/ui/button/Button.vue'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { ProcedureSrcFormData } from '~/schemas/procedure-src.schema'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import type z from 'zod'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
schema: z.ZodSchema<any>
|
||||||
|
values: any
|
||||||
|
isLoading?: boolean
|
||||||
|
isReadonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isLoading = props.isLoading !== undefined ? props.isLoading : false
|
||||||
|
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [values: ProcedureSrcFormData, resetForm: () => void]
|
||||||
|
cancel: [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defineField, errors, meta } = useForm({
|
||||||
|
validationSchema: toTypedSchema(props.schema),
|
||||||
|
initialValues: {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
parent_id: null,
|
||||||
|
} as Partial<ProcedureSrcFormData>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [code, codeAttrs] = defineField('code')
|
||||||
|
const [name, nameAttrs] = defineField('name')
|
||||||
|
const [indName, indNameAttrs] = defineField('indName')
|
||||||
|
|
||||||
|
if (props.values) {
|
||||||
|
if (props.values.code !== undefined) code.value = props.values.code
|
||||||
|
if (props.values.name !== undefined) name.value = props.values.name
|
||||||
|
if (props.values.indName !== undefined) indName.value = props.values.indName
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
code.value = ''
|
||||||
|
name.value = ''
|
||||||
|
indName.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitForm() {
|
||||||
|
const formData: ProcedureSrcFormData = {
|
||||||
|
code: code.value || '',
|
||||||
|
name: name.value || '',
|
||||||
|
indName: indName.value || null,
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelForm() {
|
||||||
|
emit('cancel', resetForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
id="form-procedure-src"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<Block
|
||||||
|
labelSize="thin"
|
||||||
|
class="!mb-2.5 !pt-0 xl:!mb-3"
|
||||||
|
:colCount="1"
|
||||||
|
>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Kode</Label>
|
||||||
|
<Field :errMessage="errors.code">
|
||||||
|
<Input
|
||||||
|
id="code"
|
||||||
|
v-model="code"
|
||||||
|
v-bind="codeAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama (FHIR)</Label>
|
||||||
|
<Field :errMessage="errors.name">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="name"
|
||||||
|
v-bind="nameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
<Label height="compact">Nama (ID)</Label>
|
||||||
|
<Field :errMessage="errors.indName">
|
||||||
|
<Input
|
||||||
|
id="indName"
|
||||||
|
v-model="indName"
|
||||||
|
v-bind="indNameAttrs"
|
||||||
|
:disabled="isLoading || isReadonly"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Cell>
|
||||||
|
</Block>
|
||||||
|
<div class="my-2 flex justify-end gap-2 py-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-[120px]"
|
||||||
|
@click="onCancelForm"
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!isReadonly"
|
||||||
|
type="button"
|
||||||
|
class="w-[120px]"
|
||||||
|
:disabled="isLoading || !meta.valid"
|
||||||
|
@click="onSubmitForm"
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
cols: [{}, {}, {}, { width: 50 }],
|
||||||
|
|
||||||
|
headers: [[{ label: 'Kode' }, { label: 'Nama (FHIR)' }, { label: 'Nama (ID)' }, { label: '' }]],
|
||||||
|
|
||||||
|
keys: ['code', 'name', 'indName', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama (FHIR)' },
|
||||||
|
{ key: 'indName', label: 'Nama (ID)' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
|
||||||
|
// Configs
|
||||||
|
import { config } from './list-cfg'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[]
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<PubMyUiDataTable
|
||||||
|
v-bind="config"
|
||||||
|
:rows="data"
|
||||||
|
:skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -132,8 +132,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppBedList
|
<AppBedList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppBuildingList
|
<AppBuildingList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppChamberList
|
<AppChamberList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppCounterList
|
<AppCounterList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { config } from '~/components/app/diagnose-src/list-cfg'
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import { DiagnoseSrcSchema, type DiagnoseSrcFormData } from '~/schemas/diagnose-src.schema'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/diagnose-src.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/diagnose-src.service'
|
||||||
|
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getItemList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async (params: any) => {
|
||||||
|
const result = await getList({
|
||||||
|
search: params.search,
|
||||||
|
sort: 'createdAt:desc',
|
||||||
|
'page-number': params['page-number'] || 0,
|
||||||
|
'page-size': params['page-size'] || 10,
|
||||||
|
})
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'diagnose-src',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Daftar Diagnosis',
|
||||||
|
icon: 'i-lucide-microscope',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClick: () => {},
|
||||||
|
onClear: () => {},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
onClick: () => {
|
||||||
|
recItem.value = null
|
||||||
|
recId.value = 0
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
isReadonly.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
|
||||||
|
const getCurrentDetail = async (id: number | string) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
if (result.success) {
|
||||||
|
const currentValue = result.body?.data || {}
|
||||||
|
recItem.value = currentValue
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Detail Diagnosis'
|
||||||
|
isReadonly.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Edit Diagnosis'
|
||||||
|
isReadonly.value = false
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getItemList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
class="mb-4 xl:mb-5"
|
||||||
|
/>
|
||||||
|
<AppDiagnoseSrcList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
:title="!!recItem ? title : 'Tambah Diagnosis'"
|
||||||
|
size="lg"
|
||||||
|
prevent-outside
|
||||||
|
@update:open="
|
||||||
|
(value: any) => {
|
||||||
|
onResetState()
|
||||||
|
isFormEntryDialogOpen = value
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AppDiagnoseSrcEntryForm
|
||||||
|
:schema="DiagnoseSrcSchema"
|
||||||
|
:values="recItem"
|
||||||
|
:is-loading="isProcessing"
|
||||||
|
:is-readonly="isReadonly"
|
||||||
|
@submit="
|
||||||
|
(values: DiagnoseSrcFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
|
if (recId > 0) {
|
||||||
|
handleActionEdit(recId, values, getItemList, resetForm, toast)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleActionSave(values, getItemList, resetForm, toast)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@cancel="handleCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recId, getItemList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p
|
||||||
|
v-for="field in config.delKeyNames"
|
||||||
|
:key="field.key"
|
||||||
|
:v-if="record?.[field.key]"
|
||||||
|
>
|
||||||
|
<span class="font-semibold">{{ field.label }}:</span>
|
||||||
|
{{ record[field.key] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -132,8 +132,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppDivisionPositionList
|
<AppDivisionPositionList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppDivisionList
|
<AppDivisionList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -96,11 +96,10 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<Separator class="my-4 xl:my-5" />
|
|
||||||
<Filter :ref-search-nav="refSearchNav" />
|
<Filter :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppEncounterList :data="data" />
|
<AppEncounterList :data="data" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog v-model:open="isFormEntryDialogOpen" title="Filter" size="lg" prevent-outside>
|
<Dialog v-model:open="isFormEntryDialogOpen" title="Filter" size="lg" prevent-outside>
|
||||||
<AppEncounterFilter />
|
<AppEncounterFilter />
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppEquipmentList
|
<AppEquipmentList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppFloorList
|
<AppFloorList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppInstallationList
|
<AppInstallationList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppMedicineGroupList :data="data" />
|
<AppMedicineGroupList :data="data" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="lg" prevent-outside>
|
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="lg" prevent-outside>
|
||||||
<AppMedicineGroupEntryForm v-model="entry" />
|
<AppMedicineGroupEntryForm v-model="entry" />
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppItemList :data="data" />
|
<AppItemList :data="data" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="xl" prevent-outside>
|
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="xl" prevent-outside>
|
||||||
<AppItemEntryForm v-model="entry" />
|
<AppItemEntryForm v-model="entry" />
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { config } from '~/components/app/medical-action-src/list-cfg'
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import { MedicalActionSrcSchema, type MedicalActionSrcFormData } from '~/schemas/medical-action-src.schema'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/medical-action-src.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/medical-action-src.service'
|
||||||
|
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getItemList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async (params: any) => {
|
||||||
|
const result = await getList({
|
||||||
|
search: params.search,
|
||||||
|
sort: 'createdAt:desc',
|
||||||
|
'page-number': params['page-number'] || 0,
|
||||||
|
'page-size': params['page-size'] || 10,
|
||||||
|
})
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'medical-action-src',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Daftar Aksi Medis',
|
||||||
|
icon: 'i-lucide-microscope',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClick: () => {},
|
||||||
|
onClear: () => {},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
onClick: () => {
|
||||||
|
recItem.value = null
|
||||||
|
recId.value = 0
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
isReadonly.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
|
||||||
|
const getCurrentDetail = async (id: number | string) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
if (result.success) {
|
||||||
|
const currentValue = result.body?.data || {}
|
||||||
|
recItem.value = currentValue
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Detail Aksi Medis'
|
||||||
|
isReadonly.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Edit Aksi Medis'
|
||||||
|
isReadonly.value = false
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getItemList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
class="mb-4 xl:mb-5"
|
||||||
|
/>
|
||||||
|
<AppMedicalActionSrcList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
:title="!!recItem ? title : 'Tambah Aksi Medis'"
|
||||||
|
size="lg"
|
||||||
|
prevent-outside
|
||||||
|
@update:open="
|
||||||
|
(value: any) => {
|
||||||
|
onResetState()
|
||||||
|
isFormEntryDialogOpen = value
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AppMedicalActionSrcEntryForm
|
||||||
|
:schema="MedicalActionSrcSchema"
|
||||||
|
:values="recItem"
|
||||||
|
:is-loading="isProcessing"
|
||||||
|
:is-readonly="isReadonly"
|
||||||
|
@submit="
|
||||||
|
(values: MedicalActionSrcFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
|
if (recId > 0) {
|
||||||
|
handleActionEdit(recId, values, getItemList, resetForm, toast)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleActionSave(values, getItemList, resetForm, toast)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@cancel="handleCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recId, getItemList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p
|
||||||
|
v-for="field in config.delKeyNames"
|
||||||
|
:key="field.key"
|
||||||
|
:v-if="record?.[field.key]"
|
||||||
|
>
|
||||||
|
<span class="font-semibold">{{ field.label }}:</span>
|
||||||
|
{{ record[field.key] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -126,8 +126,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppMedicineGroupList
|
<AppMedicineGroupList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppMedicineMethodList
|
<AppMedicineMethodList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppMedicineList
|
<AppMedicineList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { withBase } from '~/models/_base'
|
import { withBase } from '~/models/_base'
|
||||||
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
import type { PatientEntity } from '~/models/patient'
|
import type { Patient } from '~/models/patient'
|
||||||
import type { Person } from '~/models/person'
|
import type { Person } from '~/models/person'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -18,7 +18,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
// #region State & Computed
|
// #region State & Computed
|
||||||
const patient = ref(
|
const patient = ref(
|
||||||
withBase<PatientEntity>({
|
withBase<Patient>({
|
||||||
person: {} as Person,
|
person: {} as Person,
|
||||||
personAddresses: [],
|
personAddresses: [],
|
||||||
personContacts: [],
|
personContacts: [],
|
||||||
@@ -71,13 +71,10 @@ function handleAction(type: string) {
|
|||||||
<Header
|
<Header
|
||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
class="mb-4 border-b-2 border-b-slate-300 pb-2 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppPatientPreview
|
<AppPatientPreview
|
||||||
:person="patient.person"
|
:patient="patient"
|
||||||
:person-addresses="patient.personAddresses"
|
|
||||||
:person-contacts="patient.personContacts"
|
|
||||||
:person-relatives="patient.personRelatives"
|
|
||||||
@click="handleAction"
|
@click="handleAction"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PatientEntity, genPatientProps } from '~/models/patient'
|
import type { Patient, genPatientProps } from '~/models/patient'
|
||||||
import type { ExposedForm } from '~/types/form'
|
import type { ExposedForm } from '~/types/form'
|
||||||
|
import type { PatientBase } from '~/models/patient'
|
||||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
import { genPatient } from '~/models/patient'
|
import { genPatient } from '~/models/patient'
|
||||||
import { PatientSchema } from '~/schemas/patient.schema'
|
import { PatientSchema } from '~/schemas/patient.schema'
|
||||||
@@ -9,10 +10,28 @@ import { PersonAddressSchema } from '~/schemas/person-address.schema'
|
|||||||
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
|
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
|
||||||
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
|
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
|
||||||
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
|
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
|
||||||
import { postPatient } from '~/services/patient.service'
|
import { uploadAttachment } from '~/services/patient.service'
|
||||||
|
|
||||||
|
import {
|
||||||
|
// for form entry
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/patient.handler'
|
||||||
|
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
|
||||||
// #region Props & Emits
|
// #region Props & Emits
|
||||||
const payload = ref<PatientEntity>()
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const residentIdentityFile = ref<File>()
|
||||||
|
const familyCardFile = ref<File>()
|
||||||
|
|
||||||
// form related state
|
// form related state
|
||||||
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
||||||
@@ -28,13 +47,39 @@ const personPatientForm = ref<ExposedForm<any> | null>(null)
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Lifecycle Hooks
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
// Initial synchronization when forms are mounted and isSameAddress is true by default
|
||||||
|
nextTick(() => {
|
||||||
|
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
|
||||||
|
if (
|
||||||
|
(isSameAddress === true || isSameAddress === '1') &&
|
||||||
|
personAddressForm.value?.values &&
|
||||||
|
personAddressRelativeForm.value
|
||||||
|
) {
|
||||||
|
const currentAddressValues = personAddressForm.value.values
|
||||||
|
if (Object.keys(currentAddressValues).length > 0) {
|
||||||
|
personAddressRelativeForm.value.setValues(
|
||||||
|
{
|
||||||
|
...personAddressRelativeForm.value.values,
|
||||||
|
province_code: currentAddressValues.province_code || undefined,
|
||||||
|
regency_code: currentAddressValues.regency_code || undefined,
|
||||||
|
district_code: currentAddressValues.district_code || undefined,
|
||||||
|
village_code: currentAddressValues.village_code || undefined,
|
||||||
|
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||||
|
address: currentAddressValues.address || undefined,
|
||||||
|
rt: currentAddressValues.rt || undefined,
|
||||||
|
rw: currentAddressValues.rw || undefined,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Functions
|
// #region Functions
|
||||||
// #endregion region
|
async function composeFormData(): Promise<Patient> {
|
||||||
|
|
||||||
// #region Utilities & event handlers
|
|
||||||
async function submitAll() {
|
|
||||||
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
|
const [patient, address, addressRelative, families, contacts, emergencyContact] = await Promise.all([
|
||||||
personPatientForm.value?.validate(),
|
personPatientForm.value?.validate(),
|
||||||
personAddressForm.value?.validate(),
|
personAddressForm.value?.validate(),
|
||||||
@@ -50,7 +95,7 @@ async function submitAll() {
|
|||||||
|
|
||||||
// exit, if form errors happend during validation
|
// exit, if form errors happend during validation
|
||||||
// for example: dropdown not selected
|
// for example: dropdown not selected
|
||||||
if (!allValid) return
|
if (!allValid) return Promise.reject('Form validation failed')
|
||||||
|
|
||||||
const formDataRequest: genPatientProps = {
|
const formDataRequest: genPatientProps = {
|
||||||
patient: patient?.values,
|
patient: patient?.values,
|
||||||
@@ -62,46 +107,122 @@ async function submitAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = genPatient(formDataRequest)
|
const formData = genPatient(formDataRequest)
|
||||||
payload.value = formData
|
|
||||||
|
|
||||||
try {
|
if (patient?.values.residentIdentityFile) {
|
||||||
const result = await postPatient(formData)
|
residentIdentityFile.value = patient?.values.residentIdentityFile
|
||||||
if (result.success) {
|
}
|
||||||
console.log('Patient created successfully:', result.body)
|
|
||||||
// Navigate to patient list or show success message
|
if (patient?.values.familyIdentityFile) {
|
||||||
await navigateTo('/client/patient')
|
familyCardFile.value = patient?.values.familyIdentityFile
|
||||||
} else {
|
}
|
||||||
console.error('Failed to create patient:', result)
|
|
||||||
// Handle error - show error message to user
|
return new Promise((resolve) => resolve(formData))
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
const patient: Patient = await composeFormData()
|
||||||
|
let createdPatientId = 0
|
||||||
|
|
||||||
|
const response = await handleActionSave(
|
||||||
|
patient,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
toast,
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
|
if (!data) return
|
||||||
|
createdPatientId = data.id
|
||||||
|
|
||||||
|
if (residentIdentityFile.value) {
|
||||||
|
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
if (familyCardFile.value) {
|
||||||
console.error('Error creating patient:', error)
|
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
|
||||||
// Handle error - show error message to user
|
}
|
||||||
|
|
||||||
|
// If has callback provided redirect to callback with patientData
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to patient list or show success message
|
||||||
|
await navigateTo('/client/patient')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'cancel') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateTo({
|
||||||
|
name: 'client-patient',
|
||||||
|
})
|
||||||
|
// handleCancelForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Watchers
|
// #region Watchers
|
||||||
// Watcher untuk sinkronisasi alamat ketika isSameAddress = '1'
|
// Watcher untuk sinkronisasi initial ketika kedua form sudah ready
|
||||||
|
watch(
|
||||||
|
[() => personAddressForm.value, () => personAddressRelativeForm.value],
|
||||||
|
([addressForm, relativeForm]) => {
|
||||||
|
if (addressForm && relativeForm) {
|
||||||
|
// Trigger initial sync jika isSameAddress adalah true
|
||||||
|
nextTick(() => {
|
||||||
|
const isSameAddress = relativeForm.values?.isSameAddress
|
||||||
|
if ((isSameAddress === true || isSameAddress === '1') && addressForm.values) {
|
||||||
|
const currentAddressValues = addressForm.values
|
||||||
|
if (Object.keys(currentAddressValues).length > 0) {
|
||||||
|
relativeForm.setValues(
|
||||||
|
{
|
||||||
|
...relativeForm.values,
|
||||||
|
province_code: currentAddressValues.province_code || undefined,
|
||||||
|
regency_code: currentAddressValues.regency_code || undefined,
|
||||||
|
district_code: currentAddressValues.district_code || undefined,
|
||||||
|
village_code: currentAddressValues.village_code || undefined,
|
||||||
|
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||||
|
address: currentAddressValues.address || undefined,
|
||||||
|
rt: currentAddressValues.rt || undefined,
|
||||||
|
rw: currentAddressValues.rw || undefined,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher untuk sinkronisasi alamat ketika isSameAddress = true
|
||||||
watch(
|
watch(
|
||||||
() => personAddressForm.value?.values,
|
() => personAddressForm.value?.values,
|
||||||
(newAddressValues) => {
|
(newAddressValues) => {
|
||||||
// Cek apakah alamat KTP harus sama dengan alamat sekarang
|
// Cek apakah alamat KTP harus sama dengan alamat sekarang
|
||||||
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress === '1'
|
const isSameAddress = personAddressRelativeForm.value?.values?.isSameAddress
|
||||||
|
|
||||||
if (isSameAddress && newAddressValues && personAddressRelativeForm.value) {
|
if ((isSameAddress === true || isSameAddress === '1') && newAddressValues && personAddressRelativeForm.value) {
|
||||||
// Sinkronkan semua field alamat dari alamat sekarang ke alamat KTP
|
// Sinkronkan semua field alamat dari alamat sekarang ke alamat KTP
|
||||||
personAddressRelativeForm.value.setValues(
|
personAddressRelativeForm.value.setValues(
|
||||||
{
|
{
|
||||||
...personAddressRelativeForm.value.values,
|
...personAddressRelativeForm.value.values,
|
||||||
provinceId: newAddressValues.provinceId || '',
|
province_code: newAddressValues.province_code || undefined,
|
||||||
regencyId: newAddressValues.regencyId || '',
|
regency_code: newAddressValues.regency_code || undefined,
|
||||||
districtId: newAddressValues.districtId || '',
|
district_code: newAddressValues.district_code || undefined,
|
||||||
villageId: newAddressValues.villageId || '',
|
village_code: newAddressValues.village_code || undefined,
|
||||||
zipCode: newAddressValues.zipCode || '',
|
postalRegion_code: newAddressValues.postalRegion_code || undefined,
|
||||||
address: newAddressValues.address || '',
|
address: newAddressValues.address || undefined,
|
||||||
rt: newAddressValues.rt || '',
|
rt: newAddressValues.rt || undefined,
|
||||||
rw: newAddressValues.rw || '',
|
rw: newAddressValues.rw || undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -114,20 +235,24 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => personAddressRelativeForm.value?.values?.isSameAddress,
|
() => personAddressRelativeForm.value?.values?.isSameAddress,
|
||||||
(isSameAddress) => {
|
(isSameAddress) => {
|
||||||
if (isSameAddress === '1' && personAddressForm.value?.values && personAddressRelativeForm.value) {
|
if (
|
||||||
// Ketika isSameAddress diubah menjadi '1', copy alamat sekarang ke alamat KTP
|
(isSameAddress === true || isSameAddress === '1') &&
|
||||||
|
personAddressForm.value?.values &&
|
||||||
|
personAddressRelativeForm.value?.values
|
||||||
|
) {
|
||||||
|
// Ketika isSameAddress diubah menjadi true, copy alamat sekarang ke alamat KTP
|
||||||
const currentAddressValues = personAddressForm.value.values
|
const currentAddressValues = personAddressForm.value.values
|
||||||
personAddressRelativeForm.value.setValues(
|
personAddressRelativeForm.value.setValues(
|
||||||
{
|
{
|
||||||
...personAddressRelativeForm.value.values,
|
...personAddressRelativeForm.value.values,
|
||||||
provinceId: currentAddressValues.provinceId || '',
|
province_code: currentAddressValues.province_code || undefined,
|
||||||
regencyId: currentAddressValues.regencyId || '',
|
regency_code: currentAddressValues.regency_code || undefined,
|
||||||
districtId: currentAddressValues.districtId || '',
|
district_code: currentAddressValues.district_code || undefined,
|
||||||
villageId: currentAddressValues.villageId || '',
|
village_code: currentAddressValues.village_code || undefined,
|
||||||
zipCode: currentAddressValues.zipCode || '',
|
postalRegion_code: currentAddressValues.postalRegion_code || undefined,
|
||||||
address: currentAddressValues.address || '',
|
address: currentAddressValues.address || undefined,
|
||||||
rt: currentAddressValues.rt || '',
|
rt: currentAddressValues.rt || undefined,
|
||||||
rw: currentAddressValues.rw || '',
|
rw: currentAddressValues.rw || undefined,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -143,21 +268,25 @@ watch(
|
|||||||
ref="personPatientForm"
|
ref="personPatientForm"
|
||||||
:schema="PatientSchema"
|
:schema="PatientSchema"
|
||||||
/>
|
/>
|
||||||
|
<div class="h-6"></div>
|
||||||
<AppPersonAddressEntryForm
|
<AppPersonAddressEntryForm
|
||||||
ref="personAddressForm"
|
ref="personAddressForm"
|
||||||
title="Alamat Sekarang"
|
title="Alamat Sekarang"
|
||||||
:schema="PersonAddressSchema"
|
:schema="PersonAddressSchema"
|
||||||
/>
|
/>
|
||||||
|
<div class="h-6"></div>
|
||||||
<AppPersonAddressEntryFormRelative
|
<AppPersonAddressEntryFormRelative
|
||||||
ref="personAddressRelativeForm"
|
ref="personAddressRelativeForm"
|
||||||
title="Alamat KTP"
|
title="Alamat KTP"
|
||||||
:schema="PersonAddressRelativeSchema"
|
:schema="PersonAddressRelativeSchema"
|
||||||
/>
|
/>
|
||||||
|
<div class="h-6"></div>
|
||||||
<AppPersonFamilyParentsForm
|
<AppPersonFamilyParentsForm
|
||||||
ref="personFamilyForm"
|
ref="personFamilyForm"
|
||||||
title="Identitas Orang Tua"
|
title="Identitas Orang Tua"
|
||||||
:schema="PersonFamiliesSchema"
|
:schema="PersonFamiliesSchema"
|
||||||
/>
|
/>
|
||||||
|
<div class="h-6"></div>
|
||||||
<AppPersonContactEntryForm
|
<AppPersonContactEntryForm
|
||||||
ref="personContactForm"
|
ref="personContactForm"
|
||||||
title="Kontak Pasien"
|
title="Kontak Pasien"
|
||||||
@@ -171,7 +300,7 @@ watch(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="my-2 flex justify-end py-2">
|
<div class="my-2 flex justify-end py-2">
|
||||||
<Action @click="submitAll" />
|
<Action @click="handleActionClick" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -48,36 +48,37 @@ const headerPrep: HeaderPrep = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const summaryData = ref<Summary[]>([
|
// Disable dulu, ayahab kalo diminta
|
||||||
{
|
// const summaryData = ref<Summary[]>([
|
||||||
title: 'Total Pasien',
|
// {
|
||||||
icon: UsersRound,
|
// title: 'Total Pasien',
|
||||||
metric: 23,
|
// icon: UsersRound,
|
||||||
trend: 15,
|
// metric: 23,
|
||||||
timeframe: 'daily',
|
// trend: 15,
|
||||||
},
|
// timeframe: 'daily',
|
||||||
{
|
// },
|
||||||
title: 'Pasien Aktif',
|
// {
|
||||||
icon: UserCheck,
|
// title: 'Pasien Aktif',
|
||||||
metric: 100,
|
// icon: UserCheck,
|
||||||
trend: 9,
|
// metric: 100,
|
||||||
timeframe: 'daily',
|
// trend: 9,
|
||||||
},
|
// timeframe: 'daily',
|
||||||
{
|
// },
|
||||||
title: 'Kunjungan Hari Ini',
|
// {
|
||||||
icon: Calendar,
|
// title: 'Kunjungan Hari Ini',
|
||||||
metric: 52,
|
// icon: Calendar,
|
||||||
trend: 1,
|
// metric: 52,
|
||||||
timeframe: 'daily',
|
// trend: 1,
|
||||||
},
|
// timeframe: 'daily',
|
||||||
{
|
// },
|
||||||
title: 'Peserta BPJS',
|
// {
|
||||||
icon: Hospital,
|
// title: 'Peserta BPJS',
|
||||||
metric: 71,
|
// icon: Hospital,
|
||||||
trend: -3,
|
// metric: 71,
|
||||||
timeframe: 'daily',
|
// trend: -3,
|
||||||
},
|
// timeframe: 'daily',
|
||||||
])
|
// },
|
||||||
|
// ])
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Lifecycle Hooks
|
// #region Lifecycle Hooks
|
||||||
@@ -165,7 +166,9 @@ watch([recId, recAction], () => {
|
|||||||
:prep="{ ...headerPrep }"
|
:prep="{ ...headerPrep }"
|
||||||
:ref-search-nav="refSearchNav"
|
:ref-search-nav="refSearchNav"
|
||||||
/>
|
/>
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
|
<!-- Disable dulu, ayahab kalo diminta beneran -->
|
||||||
|
<!-- <div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||||
<template v-if="summaryLoading">
|
<template v-if="summaryLoading">
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
@@ -182,12 +185,14 @@ watch([recId, recAction], () => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<AppPatientList
|
|
||||||
:data="data"
|
|
||||||
:pagination-meta="paginationMeta"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<AppPatientList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<RecordConfirmation
|
<RecordConfirmation
|
||||||
v-model:open="isRecordConfirmationOpen"
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ async function getMaterialList() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
|
||||||
<AppPrescriptionList v-if="!isLoading.dataListLoading" />
|
<AppPrescriptionList v-if="!isLoading.dataListLoading" />
|
||||||
|
|
||||||
<AppPrescriptionEntry />
|
<AppPrescriptionEntry />
|
||||||
|
|
||||||
<PrescriptionItemListEntry :data=[] />
|
<PrescriptionItemListEntry :data=[] />
|
||||||
<div>
|
<div>
|
||||||
<Button>
|
<Button>
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Components
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { config } from '~/components/app/procedure-src/list-cfg'
|
||||||
|
// Types
|
||||||
|
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import { ProcedureSrcSchema, type ProcedureSrcFormData } from '~/schemas/procedure-src.schema'
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
import {
|
||||||
|
recId,
|
||||||
|
recAction,
|
||||||
|
recItem,
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleActionEdit,
|
||||||
|
handleActionRemove,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/procedure-src.handler'
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getList, getDetail } from '~/services/procedure-src.service'
|
||||||
|
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getItemList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: async (params: any) => {
|
||||||
|
const result = await getList({
|
||||||
|
search: params.search,
|
||||||
|
sort: 'createdAt:desc',
|
||||||
|
'page-number': params['page-number'] || 0,
|
||||||
|
'page-size': params['page-size'] || 10,
|
||||||
|
})
|
||||||
|
return { success: result.success || false, body: result.body || {} }
|
||||||
|
},
|
||||||
|
entityName: 'procedure-src',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'MCU Prosedur',
|
||||||
|
icon: 'i-lucide-clipboard-list',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClick: () => {},
|
||||||
|
onClear: () => {},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
onClick: () => {
|
||||||
|
recItem.value = null
|
||||||
|
recId.value = 0
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
isReadonly.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
|
||||||
|
const getCurrentDetail = async (id: number | string) => {
|
||||||
|
const result = await getDetail(id)
|
||||||
|
if (result.success) {
|
||||||
|
const currentValue = result.body?.data || {}
|
||||||
|
recItem.value = currentValue
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Detail Prosedur'
|
||||||
|
isReadonly.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
getCurrentDetail(recId.value)
|
||||||
|
title.value = 'Edit Prosedur'
|
||||||
|
isReadonly.value = false
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await getItemList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header
|
||||||
|
v-model="searchInput"
|
||||||
|
:prep="headerPrep"
|
||||||
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
|
@search="handleSearch"
|
||||||
|
class="mb-4 xl:mb-5"
|
||||||
|
/>
|
||||||
|
<AppProcedureSrcList
|
||||||
|
:data="data"
|
||||||
|
:pagination-meta="paginationMeta"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
:title="!!recItem ? title : 'Tambah Prosedur'"
|
||||||
|
size="lg"
|
||||||
|
prevent-outside
|
||||||
|
@update:open="
|
||||||
|
(value: any) => {
|
||||||
|
onResetState()
|
||||||
|
isFormEntryDialogOpen = value
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AppProcedureSrcEntryForm
|
||||||
|
:schema="ProcedureSrcSchema"
|
||||||
|
:values="recItem"
|
||||||
|
:is-loading="isProcessing"
|
||||||
|
:is-readonly="isReadonly"
|
||||||
|
@submit="
|
||||||
|
(values: ProcedureSrcFormData | Record<string, any>, resetForm: () => void) => {
|
||||||
|
if (recId > 0) {
|
||||||
|
handleActionEdit(recId, values, getItemList, resetForm, toast)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleActionSave(values, getItemList, resetForm, toast)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@cancel="handleCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="recItem"
|
||||||
|
@confirm="() => handleActionRemove(recId, getItemList, toast)"
|
||||||
|
@cancel=""
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p
|
||||||
|
v-for="field in config.delKeyNames"
|
||||||
|
:key="field.key"
|
||||||
|
:v-if="record?.[field.key]"
|
||||||
|
>
|
||||||
|
<span class="font-semibold">{{ field.label }}:</span>
|
||||||
|
{{ record[field.key] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -129,8 +129,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppPublicScreenList
|
<AppPublicScreenList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppRoomList
|
<AppRoomList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -108,10 +108,12 @@ const activeTabFilter = computed({
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-md border p-4">
|
<div class="rounded-md border p-4">
|
||||||
<Header :prep="headerPrep" :ref-search-nav="refSearchNav" />
|
<Header :prep="headerPrep" :ref-search-nav="refSearchNav" />
|
||||||
|
|
||||||
<div class="my-4 flex flex-1 flex-col gap-3 md:gap-4">
|
<div class="my-4 flex flex-1 flex-col gap-3 md:gap-4">
|
||||||
<PubMyUiServiceStatus v-bind="service" />
|
<PubMyUiServiceStatus v-bind="service" />
|
||||||
<AppSatusehatCardSummary :is-loading="isLoading.satusehatConn!" :summary-data="summaryData" />
|
<AppSatusehatCardSummary :is-loading="isLoading.satusehatConn!" :summary-data="summaryData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md border p-4">
|
<div class="rounded-md border p-4">
|
||||||
<h2 class="text-md py-2 font-semibold">FHIR Resource</h2>
|
<h2 class="text-md py-2 font-semibold">FHIR Resource</h2>
|
||||||
<Tabs v-model="activeTabFilter">
|
<Tabs v-model="activeTabFilter">
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AssesmentFunctionList :data="data" />
|
<AssesmentFunctionList :data="data" />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -57,7 +57,6 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -130,8 +130,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppSpecialistList
|
<AppSpecialistList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppSubSpecialistList
|
<AppSubSpecialistList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppToolsList
|
<AppToolsList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppUnitList
|
<AppUnitList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -126,15 +126,13 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
<div class="rounded-md border p-4">
|
|
||||||
<AppUomList
|
<AppUomList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:open="isFormEntryDialogOpen"
|
v-model:open="isFormEntryDialogOpen"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ provide('table_data_loader', isLoading)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
|
||||||
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -132,8 +132,8 @@ onMounted(async () => {
|
|||||||
:prep="headerPrep"
|
:prep="headerPrep"
|
||||||
:ref-search-nav="headerPrep.refSearchNav"
|
:ref-search-nav="headerPrep.refSearchNav"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
class="mb-4 xl:mb-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppWarehouseList
|
<AppWarehouseList
|
||||||
:data="data"
|
:data="data"
|
||||||
:pagination-meta="paginationMeta"
|
:pagination-meta="paginationMeta"
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { type Item } from './index'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id?: string
|
||||||
|
modelValue?: string
|
||||||
|
items: Item[]
|
||||||
|
placeholder?: string
|
||||||
|
searchPlaceholder?: string
|
||||||
|
emptyMessage?: string
|
||||||
|
class?: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
page?: number
|
||||||
|
totalPage?: number
|
||||||
|
hasNext?: boolean
|
||||||
|
hasPrev?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
searchText?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
'update:searchText': [value: string]
|
||||||
|
'page-change': [page: number]
|
||||||
|
next: []
|
||||||
|
prev: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const internalSearchText = ref(props.searchText || '')
|
||||||
|
|
||||||
|
// Keep internal search text synced with props
|
||||||
|
watch(
|
||||||
|
() => props.searchText,
|
||||||
|
(val) => {
|
||||||
|
if (val !== internalSearchText.value) internalSearchText.value = val || ''
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedItem = computed(() => props.items.find((item) => item.value === props.modelValue))
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (selectedItem.value?.label) return selectedItem.value.label
|
||||||
|
return props.placeholder || 'Pilih item'
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchableItems = computed(() => {
|
||||||
|
const itemsWithSearch = props.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
searchValue: `${item.code || ''} ${item.label}`.trim(),
|
||||||
|
isSelected: item.value === props.modelValue,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return itemsWithSearch.sort((a, b) => {
|
||||||
|
const aPriority = a.priority ?? 0
|
||||||
|
const bPriority = b.priority ?? 0
|
||||||
|
if (aPriority !== bPriority) return bPriority - aPriority
|
||||||
|
if (a.isSelected && !b.isSelected) return -1
|
||||||
|
if (!a.isSelected && b.isSelected) return 1
|
||||||
|
return a.label.localeCompare(b.label)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSelect(item: Item) {
|
||||||
|
emit('update:modelValue', item.value)
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchInput(value: string) {
|
||||||
|
console.log('[ComboboxPaginated] emit update:searchText', value)
|
||||||
|
internalSearchText.value = value
|
||||||
|
emit('update:searchText', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
if (props.hasPrev) emit('prev')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
if (props.hasNext) emit('next')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Popover v-model:open="open">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
:id="props.id"
|
||||||
|
:disabled="props.isDisabled"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
:aria-expanded="open"
|
||||||
|
:aria-controls="`${props.id}-list`"
|
||||||
|
:aria-describedby="`${props.id}-search`"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'h-8 w-full justify-between rounded-md border px-3 font-normal focus:outline-none focus:ring-1 focus:ring-black dark:!border-slate-400 dark:focus:ring-white md:text-xs 2xl:h-9 2xl:text-sm',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed border-gray-300 bg-gray-100 text-gray-500 opacity-50': props.isDisabled,
|
||||||
|
'border-gray-400 bg-white text-black hover:bg-gray-50 dark:border-gray-600 dark:bg-slate-950 dark:text-white dark:hover:bg-gray-700':
|
||||||
|
!props.isDisabled,
|
||||||
|
'text-gray-400 dark:text-gray-500': !props.modelValue && !props.isDisabled,
|
||||||
|
},
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ displayText }}
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-chevrons-up-down"
|
||||||
|
:class="
|
||||||
|
cn('ml-2 h-4 w-4 shrink-0', {
|
||||||
|
'opacity-30': props.isDisabled,
|
||||||
|
'text-gray-500 opacity-50 dark:text-gray-300': !props.isDisabled,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
class="w-[var(--radix-popover-trigger-width)] border border-gray-200 bg-white p-0 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Command class="bg-white dark:bg-gray-800">
|
||||||
|
<CommandInput
|
||||||
|
:id="`${props.id}-search`"
|
||||||
|
class="h-9 border-0 border-b border-gray-200 bg-white text-black focus:ring-0 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
:placeholder="searchPlaceholder || 'Cari...'"
|
||||||
|
v-model="internalSearchText"
|
||||||
|
@input="onSearchInput(($event.target as HTMLInputElement).value)"
|
||||||
|
:aria-label="`Cari ${displayText}`"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommandEmpty class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ emptyMessage || 'Item tidak ditemukan.' }}
|
||||||
|
</CommandEmpty>
|
||||||
|
|
||||||
|
<CommandList
|
||||||
|
:id="`${props.id}-list`"
|
||||||
|
role="listbox"
|
||||||
|
class="max-h-60 overflow-auto"
|
||||||
|
>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
v-for="item in searchableItems"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 md:text-xs xl:text-sm',
|
||||||
|
'text-black focus:outline-none dark:text-white',
|
||||||
|
'hover:bg-primary hover:text-white focus:bg-primary focus:text-white',
|
||||||
|
'data-[highlighted]:bg-primary data-[highlighted]:text-white',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@select="onSelect(item)"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
v-if="item.code"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ item.code }}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-check"
|
||||||
|
:class="cn('h-4 w-4', props.modelValue === item.value ? 'opacity-100' : 'opacity-0')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between border-t border-gray-200 p-2 text-xs dark:border-gray-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
v-if="props.isLoading"
|
||||||
|
name="i-lucide-loader-2"
|
||||||
|
class="h-3 w-3 animate-spin"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ props.page || 1 }} / {{ props.totalPage || 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="!props.hasPrev || props.isDisabled"
|
||||||
|
@click="handlePrev"
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="!props.hasNext || props.isDisabled"
|
||||||
|
@click="handleNext"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
@@ -23,7 +23,7 @@ const open = ref(false)
|
|||||||
const selectedItem = computed(() => props.items.find((item) => item.value === props.modelValue))
|
const selectedItem = computed(() => props.items.find((item) => item.value === props.modelValue))
|
||||||
|
|
||||||
const displayText = computed(() => {
|
const displayText = computed(() => {
|
||||||
console.log(selectedItem);
|
console.log(selectedItem)
|
||||||
if (selectedItem.value?.label) {
|
if (selectedItem.value?.label) {
|
||||||
return selectedItem.value.label
|
return selectedItem.value.label
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ const displayText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(props, () => {
|
watch(props, () => {
|
||||||
console.log(props.modelValue);
|
console.log(props.modelValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
const searchableItems = computed(() => {
|
const searchableItems = computed(() => {
|
||||||
@@ -74,10 +74,11 @@ function onSelect(item: Item) {
|
|||||||
:aria-describedby="`${props.id}-search`"
|
:aria-describedby="`${props.id}-search`"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'w-full justify-between border dark:!border-slate-400 h-8 2xl:h-9 md:text-xs 2xl:text-sm font-normal rounded-md px-3 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
'h-8 w-full justify-between rounded-md border px-3 font-normal focus:outline-none focus:ring-1 focus:ring-black dark:!border-slate-400 dark:focus:ring-white md:text-xs 2xl:h-9 2xl:text-sm',
|
||||||
{
|
{
|
||||||
'cursor-not-allowed bg-gray-100 opacity-50 border-gray-300 text-gray-500': props.isDisabled,
|
'cursor-not-allowed border-gray-300 bg-gray-100 text-gray-500 opacity-50': props.isDisabled,
|
||||||
'bg-white text-black dark:bg-slate-950 dark:text-white dark:border-gray-600 border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700': !props.isDisabled,
|
'border-gray-400 bg-white text-black hover:bg-gray-50 dark:border-gray-600 dark:bg-slate-950 dark:text-white dark:hover:bg-gray-700':
|
||||||
|
!props.isDisabled,
|
||||||
'text-gray-400 dark:text-gray-500': !modelValue && !props.isDisabled,
|
'text-gray-400 dark:text-gray-500': !modelValue && !props.isDisabled,
|
||||||
},
|
},
|
||||||
props.class,
|
props.class,
|
||||||
@@ -87,22 +88,26 @@ function onSelect(item: Item) {
|
|||||||
{{ displayText }}
|
{{ displayText }}
|
||||||
<Icon
|
<Icon
|
||||||
name="i-lucide-chevrons-up-down"
|
name="i-lucide-chevrons-up-down"
|
||||||
:class="cn('ml-2 h-4 w-4 shrink-0', {
|
:class="
|
||||||
'opacity-30': props.isDisabled,
|
cn('ml-2 h-4 w-4 shrink-0', {
|
||||||
'opacity-50 text-gray-500 dark:text-gray-300': !props.isDisabled
|
'opacity-30': props.isDisabled,
|
||||||
})"
|
'text-gray-500 opacity-50 dark:text-gray-300': !props.isDisabled,
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-[var(--radix-popover-trigger-width)] p-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
<PopoverContent
|
||||||
|
class="w-[var(--radix-popover-trigger-width)] border border-gray-200 bg-white p-0 dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
<Command class="bg-white dark:bg-gray-800">
|
<Command class="bg-white dark:bg-gray-800">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
:id="`${props.id}-search`"
|
:id="`${props.id}-search`"
|
||||||
class="h-9 bg-white dark:bg-gray-800 text-black dark:text-white border-0 border-b border-gray-200 dark:border-gray-700 focus:ring-0"
|
class="h-9 border-0 border-b border-gray-200 bg-white text-black focus:ring-0 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
:placeholder="searchPlaceholder || 'Cari...'"
|
:placeholder="searchPlaceholder || 'Cari...'"
|
||||||
:aria-label="`Cari ${displayText}`"
|
:aria-label="`Cari ${displayText}`"
|
||||||
/>
|
/>
|
||||||
<CommandEmpty class="text-gray-500 dark:text-gray-400 py-6 text-center text-sm">
|
<CommandEmpty class="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{ emptyMessage || 'Item tidak ditemukan.' }}
|
{{ emptyMessage || 'Item tidak ditemukan.' }}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandList
|
<CommandList
|
||||||
@@ -118,7 +123,7 @@ function onSelect(item: Item) {
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 md:text-xs xl:text-sm',
|
'flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 md:text-xs xl:text-sm',
|
||||||
'focus:outline-none text-black dark:text-white',
|
'text-black focus:outline-none dark:text-white',
|
||||||
'hover:bg-primary hover:text-white focus:bg-primary focus:text-white',
|
'hover:bg-primary hover:text-white focus:bg-primary focus:text-white',
|
||||||
'data-[highlighted]:bg-primary data-[highlighted]:text-white',
|
'data-[highlighted]:bg-primary data-[highlighted]:text-white',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ onMounted(() => {
|
|||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button variant="outline" class="bg-white border-gray-400 font-normal text-right h-[40px] w-full">
|
<Button variant="outline" class="bg-white border-gray-400 font-normal text-right w-full h-8 2xl:h-9">
|
||||||
<div class="flex justify-between items-center w-full">
|
<div class="flex justify-between items-center w-full">
|
||||||
<p v-if="date">{{ format(date, 'PPP', { locale: localeID }) }}</p>
|
<p v-if="date">{{ format(date, 'PPP', { locale: localeID }) }}</p>
|
||||||
<p v-else class="text-sm text-black text-opacity-50">{{ props.placeholder || 'Tanggal' }}</p>
|
<p v-else class="text-black text-opacity-50">{{ props.placeholder || 'Tanggal' }}</p>
|
||||||
<Icon name="i-lucide-calendar" class="h-5 w-5" />
|
<Icon name="i-lucide-calendar"/>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const getLabelSizeIdx = (size: string) => {
|
|||||||
const settingClass = computed(() => {
|
const settingClass = computed(() => {
|
||||||
const breakPointIdx = getBreakpointIdx(props.gridPoint)
|
const breakPointIdx = getBreakpointIdx(props.gridPoint)
|
||||||
let cls = breakpoints[breakPointIdx]
|
let cls = breakpoints[breakPointIdx]
|
||||||
cls += ' gap-x-4 xl:gap-x-5 gap-y-2 xl:gap-y-3 ' + [
|
cls += ' gap-x-4 2xl:gap-x-5 ' + [
|
||||||
'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5',
|
'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5',
|
||||||
'grid-cols-6', 'grid-cols-7', 'grid-cols-8', 'grid-cols-9', 'grid-cols-10',
|
'grid-cols-6', 'grid-cols-7', 'grid-cols-8', 'grid-cols-9', 'grid-cols-10',
|
||||||
][props.colCount - 1]
|
][props.colCount - 1]
|
||||||
@@ -44,7 +44,7 @@ const settingClass = computed(() => {
|
|||||||
' [&_.cell]:mb-3 [&_.cell]:2xl:mb-0',
|
' [&_.cell]:mb-3 [&_.cell]:2xl:mb-0',
|
||||||
][breakPointIdx]
|
][breakPointIdx]
|
||||||
if (props.cellFlex) {
|
if (props.cellFlex) {
|
||||||
cls += ' ' + [
|
cls += ' gap-y-2 2xl:gap-y-3 ' + [
|
||||||
'[&_.cell]:flex',
|
'[&_.cell]:flex',
|
||||||
'[&_.cell]:sm:flex',
|
'[&_.cell]:sm:flex',
|
||||||
'[&_.cell]:md:flex',
|
'[&_.cell]:md:flex',
|
||||||
@@ -60,11 +60,11 @@ const settingClass = computed(() => {
|
|||||||
'[&_.label]:md:w-44 [&_.label]:xl:w-52',
|
'[&_.label]:md:w-44 [&_.label]:xl:w-52',
|
||||||
][getLabelSizeIdx(props.labelSize)]
|
][getLabelSizeIdx(props.labelSize)]
|
||||||
} else {
|
} else {
|
||||||
cls += ' [&_.label]:pb-1 [&_.label]:!pt-0 ';
|
cls += ' gap-y-4 2xl:gap-y-5 [&_.label]:pb-1 [&_.label]:!pt-0 ';
|
||||||
}
|
}
|
||||||
cls += ' [&:not(.preview)_.height-default]:pt-2 [&:not(.preview)_.height-default]:2xl:!pt-1.5 [&:not(.preview)_.height-compact]:!pt-1 '
|
cls += ' [&:not(.preview)_.height-default]:pt-2 [&:not(.preview)_.height-default]:2xl:!pt-1.5 [&:not(.preview)_.height-compact]:!pt-1 '
|
||||||
cls += '[&_textarea]:text-xs [&_textarea]:2xl:!text-sm '
|
cls += '[&_textarea]:md:text-xs [&_textarea]:2xl:!text-sm '
|
||||||
cls += '[&_label]:text-xs [&_label]:2xl:!text-sm'
|
cls += '[&_label]:md:text-xs [&_label]:md:text-xs [&_label]:2xl:!text-sm'
|
||||||
return cls
|
return cls
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ const props = defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<div :class="`field ${props.defaultClass} ${props.class}`">
|
<div :class="`field ${props.defaultClass} ${props.class}`">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="props.errMessage" class="mt-1 md:text-xs 2xl:text-sm font-medium text-red-500">{{ props.errMessage }}</div>
|
<div v-if="props.errMessage" class="mt-1 md:!text-xs 2xl:!text-sm font-medium text-red-500">{{ props.errMessage }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||||
|
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||||
|
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||||
|
import { Input } from '~/components/pub/ui/input'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fieldName: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
hint?: string
|
||||||
|
modelValue?: File | null
|
||||||
|
accept?: string[]
|
||||||
|
maxSizeMb?: number
|
||||||
|
errors?: FormErrors
|
||||||
|
class?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
icons?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hintMsg = computed(() => {
|
||||||
|
if (props.hint) return props.hint
|
||||||
|
if (props.accept) {
|
||||||
|
return `${props.accept.map((ext) => '.' + ext.replace(/^\./, '')).join(', ')}, maksimal ${props.maxSizeMb} MB`
|
||||||
|
}
|
||||||
|
return 'Gunakan file yang sesuai'
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onFileChange(event: Event, handleChange: (value: any) => void) {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
const file = target.files?.[0]
|
||||||
|
|
||||||
|
handleChange(file)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DE.Cell>
|
||||||
|
<DE.Label
|
||||||
|
v-if="label !== ''"
|
||||||
|
:label-for="fieldName"
|
||||||
|
:is-required="isRequired && !isDisabled"
|
||||||
|
>
|
||||||
|
{{ label }} ({{ hintMsg }})
|
||||||
|
</DE.Label>
|
||||||
|
<DE.Field
|
||||||
|
:id="fieldName"
|
||||||
|
:errors="errors"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
v-slot="{ componentField, handleChange }"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<FormControl class="flex flex-col">
|
||||||
|
<Input
|
||||||
|
@change="onFileChange($event, handleChange)"
|
||||||
|
type="file"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
v-bind="componentField"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</DE.Field>
|
||||||
|
</DE.Cell>
|
||||||
|
</template>
|
||||||
@@ -6,12 +6,15 @@ import Label from '~/components/pub/my-ui/form/label.vue'
|
|||||||
import { Input } from '~/components/pub/ui/input'
|
import { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName: string
|
||||||
placeholder: string
|
placeholder: string
|
||||||
label: string
|
label: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
class?: string
|
class?: string
|
||||||
|
colSpan?: number
|
||||||
numericOnly?: boolean
|
numericOnly?: boolean
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
@@ -42,15 +45,15 @@ function handleInput(event: Event) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldGroup>
|
<DE.Cell :col-span="colSpan || 1">
|
||||||
<Label
|
<DE.Label
|
||||||
v-if="label !== ''"
|
v-if="label !== ''"
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</Label>
|
</DE.Label>
|
||||||
<Field
|
<DE.Field
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
>
|
>
|
||||||
@@ -77,6 +80,6 @@ function handleInput(event: Event) {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Field>
|
</DE.Field>
|
||||||
</FieldGroup>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,7 +46,7 @@ const wrapperClass = computed(() => [
|
|||||||
props.stacked ? '' : positionWrapMap[props.position],
|
props.stacked ? '' : positionWrapMap[props.position],
|
||||||
])
|
])
|
||||||
|
|
||||||
const labelClass = computed(() => [props.stacked ? 'block mb-1 text-sm font-normal' : positionChildMap[props.position]])
|
const labelClass = computed(() => [props.stacked ? 'block mb-1 font-normal' : positionChildMap[props.position]])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SelectTrigger from '~/components/pub/ui/select/SelectTrigger.vue'
|
|||||||
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
|
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
interface Item {
|
export interface SelectItem {
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
code?: string
|
code?: string
|
||||||
@@ -19,7 +19,7 @@ interface Item {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
items: Item[]
|
items: SelectItem[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
label?: string
|
label?: string
|
||||||
separator?: boolean
|
separator?: boolean
|
||||||
@@ -116,7 +116,7 @@ watch(
|
|||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
'rounded-md focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
||||||
{
|
{
|
||||||
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
||||||
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
||||||
@@ -132,7 +132,7 @@ watch(
|
|||||||
<SelectValue
|
<SelectValue
|
||||||
:placeholder="placeholder || 'Pilih item'"
|
:placeholder="placeholder || 'Pilih item'"
|
||||||
:class="
|
:class="
|
||||||
cn('text-sm', {
|
cn('', {
|
||||||
'text-gray-400': !props.modelValue,
|
'text-gray-400': !props.modelValue,
|
||||||
'text-black dark:text-white': props.modelValue,
|
'text-black dark:text-white': props.modelValue,
|
||||||
'text-gray-500': isDisabled,
|
'text-gray-500': isDisabled,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function onFilterClick() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2 mb-4 2xl:mb-5">
|
||||||
<div class="relative w-64">
|
<div class="relative w-64">
|
||||||
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
|
||||||
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ function btnClick() {
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between"
|
class="flex items-center justify-between mb-4 2xl:mb-5"
|
||||||
:class="cn('', props.class)"
|
:class="cn('', props.class)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="font-semibold text-gray-900 md:text-base xl:text-lg">
|
<div class="font-semibold text-gray-900 md:text-base 2xl:text-lg">
|
||||||
<Icon
|
<Icon
|
||||||
:name="props.prep.icon!"
|
:name="props.prep.icon!"
|
||||||
class="mr-2 align-middle md:size-6"
|
class="mr-2 align-middle md:size-6"
|
||||||
@@ -66,4 +66,5 @@ function btnClick() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<Separator class="my-4 xl:my-5" />
|
||||||
</template>
|
</template>
|
||||||
@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground flex h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:text-xs xl:file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:!text-xs xl:file:!text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
|||||||
|
|
||||||
<SelectContent class="rounded border bg-white">
|
<SelectContent class="rounded border bg-white">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel v-if="label" class="px-2 py-1 text-sm font-semibold">
|
<SelectLabel v-if="label" class="px-2 py-1 text-xs 2xl:text-sm font-semibold">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
|||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-xs 2xl:text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectLabel :class="cn('px-2 py-1.5 text-sm font-semibold', props.class)">
|
<SelectLabel :class="cn('px-2 py-1.5 text-xs 2xl:text-sm font-semibold', props.class)">
|
||||||
<slot />
|
<slot />
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,7 +21,7 @@ const iconName = computed(() => props.iconName || 'i-radix-icons-caret-sort')
|
|||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'border-input ring-offset-background placeholder:text-muted-foreground relative flex h-9 md:h-8 xl:h-9 w-full rounded-md border border-gray-400 pl-3 pr-8 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
'border-input ring-offset-background placeholder:text-muted-foreground relative flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 pl-3 pr-8 py-2 text-xs 2xl:text-sm file:border-0 file:bg-transparent file:text-xs file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
import type { District } from '~/models/district'
|
||||||
|
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { toTitleCase } from '~/lib/utils'
|
||||||
|
import * as districtService from '~/services/district.service'
|
||||||
|
|
||||||
|
// Global cache untuk districts berdasarkan regency code
|
||||||
|
const districtsCache = ref<Map<string, District[]>>(new Map())
|
||||||
|
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||||
|
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||||
|
|
||||||
|
export function useDistricts(regencyCode: Ref<string | undefined> | string | undefined) {
|
||||||
|
// Convert regencyCode ke ref jika bukan ref
|
||||||
|
const regencyCodeRef = typeof regencyCode === 'string' || regencyCode === undefined ? ref(regencyCode) : regencyCode
|
||||||
|
|
||||||
|
// Computed untuk mendapatkan districts berdasarkan regency code
|
||||||
|
const districts = computed(() => {
|
||||||
|
const code = regencyCodeRef.value
|
||||||
|
if (!code) return []
|
||||||
|
return districtsCache.value.get(code) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk loading state
|
||||||
|
const isLoading = computed(() => {
|
||||||
|
const code = regencyCodeRef.value
|
||||||
|
if (!code) return false
|
||||||
|
return loadingStates.value.get(code) || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk error state
|
||||||
|
const error = computed(() => {
|
||||||
|
const code = regencyCodeRef.value
|
||||||
|
if (!code) return null
|
||||||
|
return errorStates.value.get(code) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk format SelectItem
|
||||||
|
const districtOptions = computed<SelectItem[]>(() => {
|
||||||
|
return districts.value.map((district) => ({
|
||||||
|
label: toTitleCase(district.name),
|
||||||
|
value: district.code,
|
||||||
|
searchValue: `${district.code} ${district.name}`.trim(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function untuk fetch districts berdasarkan regency code
|
||||||
|
async function fetchDistricts(regencyCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||||
|
const code = regencyCodeParam || regencyCodeRef.value
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
|
// Jika user action atau force refresh, selalu fetch
|
||||||
|
// Jika bukan user action dan sudah ada cache, skip
|
||||||
|
if (!isUserAction && !forceRefresh && districtsCache.value.has(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||||
|
if (loadingStates.value.get(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahan: Cek apakah ada pending request untuk code yang sama
|
||||||
|
const pendingKey = `pending_${code}`
|
||||||
|
if (loadingStates.value.get(pendingKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingStates.value.set(pendingKey, true)
|
||||||
|
|
||||||
|
loadingStates.value.set(code, true)
|
||||||
|
errorStates.value.set(code, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await districtService.getList({
|
||||||
|
sort: 'name:asc',
|
||||||
|
'regency-code': code,
|
||||||
|
'page-no-limit': true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const districtsData = response.body.data || []
|
||||||
|
districtsCache.value.set(code, districtsData)
|
||||||
|
} else {
|
||||||
|
errorStates.value.set(code, 'Gagal memuat data kecamatan')
|
||||||
|
console.error('Failed to fetch districts:', response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kecamatan')
|
||||||
|
console.error('Error fetching districts:', err)
|
||||||
|
} finally {
|
||||||
|
loadingStates.value.set(code, false)
|
||||||
|
loadingStates.value.delete(pendingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari district berdasarkan code
|
||||||
|
function getDistrictByCode(code: string): District | undefined {
|
||||||
|
const regencyCode = regencyCodeRef.value
|
||||||
|
if (!regencyCode) return undefined
|
||||||
|
|
||||||
|
const districtsForRegency = districtsCache.value.get(regencyCode) || []
|
||||||
|
return districtsForRegency.find((district) => district.code === code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari district berdasarkan name
|
||||||
|
function getDistrictByName(name: string): District | undefined {
|
||||||
|
const regencyCode = regencyCodeRef.value
|
||||||
|
if (!regencyCode) return undefined
|
||||||
|
|
||||||
|
const districtsForRegency = districtsCache.value.get(regencyCode) || []
|
||||||
|
return districtsForRegency.find((district) => district.name.toLowerCase() === name.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear cache regency tertentu
|
||||||
|
function clearCache(regencyCodeParam?: string) {
|
||||||
|
const code = regencyCodeParam || regencyCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
districtsCache.value.delete(code)
|
||||||
|
loadingStates.value.delete(code)
|
||||||
|
errorStates.value.delete(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear semua cache
|
||||||
|
function clearAllCache() {
|
||||||
|
districtsCache.value.clear()
|
||||||
|
loadingStates.value.clear()
|
||||||
|
errorStates.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk refresh data
|
||||||
|
function refreshDistricts(regencyCodeParam?: string) {
|
||||||
|
const code = regencyCodeParam || regencyCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
return fetchDistricts(code, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced regency code untuk mencegah multiple calls
|
||||||
|
const debouncedRegencyCode = refDebounced(regencyCodeRef, 100)
|
||||||
|
|
||||||
|
// Watch perubahan regency code untuk auto fetch
|
||||||
|
watch(
|
||||||
|
debouncedRegencyCode,
|
||||||
|
(newCode, oldCode) => {
|
||||||
|
if (newCode && newCode !== oldCode) {
|
||||||
|
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||||
|
const isUserAction = !!oldCode
|
||||||
|
fetchDistricts(newCode, false, isUserAction)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
districts: readonly(districts),
|
||||||
|
districtOptions,
|
||||||
|
|
||||||
|
// State
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
error: readonly(error),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
fetchDistricts,
|
||||||
|
refreshDistricts,
|
||||||
|
getDistrictByCode,
|
||||||
|
getDistrictByName,
|
||||||
|
clearCache,
|
||||||
|
clearAllCache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk direct access ke cached data (jika diperlukan)
|
||||||
|
export const useDistrictsCache = () => ({
|
||||||
|
districtsCache: readonly(districtsCache),
|
||||||
|
loadingStates: readonly(loadingStates),
|
||||||
|
errorStates: readonly(errorStates),
|
||||||
|
})
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { ref, computed, watch, readonly, type Ref } from 'vue'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
import type { PostalRegion } from '~/models/postal-region'
|
||||||
|
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import * as postalRegionService from '~/services/postal-region.service'
|
||||||
|
|
||||||
|
// Global cache untuk postal codes berdasarkan village code
|
||||||
|
const postalRegionCache = ref<Map<string, PostalRegion[]>>(new Map())
|
||||||
|
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||||
|
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||||
|
|
||||||
|
export function usePostalRegion(villageCode: Ref<string | undefined> | string | undefined) {
|
||||||
|
// Convert villageCode ke ref jika bukan ref
|
||||||
|
const villageCodeRef = typeof villageCode === 'string' || villageCode === undefined ? ref(villageCode) : villageCode
|
||||||
|
|
||||||
|
// Computed untuk mendapatkan postalRegion berdasarkan village code
|
||||||
|
const postalRegion = computed(() => {
|
||||||
|
const code = villageCodeRef.value
|
||||||
|
if (!code) return []
|
||||||
|
return postalRegionCache.value.get(code) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk loading state
|
||||||
|
const isLoading = computed(() => {
|
||||||
|
const code = villageCodeRef.value
|
||||||
|
if (!code) return false
|
||||||
|
return loadingStates.value.get(code) || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk error state
|
||||||
|
const error = computed(() => {
|
||||||
|
const code = villageCodeRef.value
|
||||||
|
if (!code) return null
|
||||||
|
return errorStates.value.get(code) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk format SelectItem
|
||||||
|
const postalRegionOptions = computed<SelectItem[]>(() => {
|
||||||
|
return postalRegion.value.map((postalRegion) => ({
|
||||||
|
label: postalRegion.code,
|
||||||
|
value: postalRegion.code,
|
||||||
|
searchValue: postalRegion.code,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function untuk fetch postalRegion berdasarkan village code
|
||||||
|
async function fetchpostalRegion(villageCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||||
|
const code = villageCodeParam || villageCodeRef.value
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
|
// Jika user action atau force refresh, selalu fetch
|
||||||
|
// Jika bukan user action dan sudah ada cache, skip
|
||||||
|
if (!isUserAction && !forceRefresh && postalRegionCache.value.has(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||||
|
if (loadingStates.value.get(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahan: Cek apakah ada pending request untuk code yang sama
|
||||||
|
const pendingKey = `pending_${code}`
|
||||||
|
if (loadingStates.value.get(pendingKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingStates.value.set(pendingKey, true)
|
||||||
|
|
||||||
|
loadingStates.value.set(code, true)
|
||||||
|
errorStates.value.set(code, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await postalRegionService.getList({
|
||||||
|
sort: 'code:asc',
|
||||||
|
'village-code': code,
|
||||||
|
'page-no-limit': true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const postalRegionData = response.body.data || []
|
||||||
|
postalRegionCache.value.set(code, postalRegionData)
|
||||||
|
} else {
|
||||||
|
errorStates.value.set(code, 'Gagal memuat data kode pos')
|
||||||
|
console.error('Failed to fetch postal codes:', response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kode pos')
|
||||||
|
console.error('Error fetching postal codes:', err)
|
||||||
|
} finally {
|
||||||
|
loadingStates.value.set(code, false)
|
||||||
|
loadingStates.value.delete(pendingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari postalRegion berdasarkan code
|
||||||
|
function getpostalRegionByCode(code: string): PostalRegion | undefined {
|
||||||
|
const villageCode = villageCodeRef.value
|
||||||
|
if (!villageCode) return undefined
|
||||||
|
|
||||||
|
const postalRegionForVillage = postalRegionCache.value.get(villageCode) || []
|
||||||
|
return postalRegionForVillage.find((postalRegion) => postalRegion.code === code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear cache village tertentu
|
||||||
|
function clearCache(villageCodeParam?: string) {
|
||||||
|
const code = villageCodeParam || villageCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
postalRegionCache.value.delete(code)
|
||||||
|
loadingStates.value.delete(code)
|
||||||
|
errorStates.value.delete(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear semua cache
|
||||||
|
function clearAllCache() {
|
||||||
|
postalRegionCache.value.clear()
|
||||||
|
loadingStates.value.clear()
|
||||||
|
errorStates.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk refresh data
|
||||||
|
function refreshpostalRegion(villageCodeParam?: string) {
|
||||||
|
const code = villageCodeParam || villageCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
return fetchpostalRegion(code, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced village code untuk mencegah multiple calls
|
||||||
|
const debouncedVillageCode = refDebounced(villageCodeRef, 100)
|
||||||
|
|
||||||
|
// Watch perubahan village code untuk auto fetch
|
||||||
|
watch(
|
||||||
|
debouncedVillageCode,
|
||||||
|
(newCode, oldCode) => {
|
||||||
|
if (newCode && newCode !== oldCode) {
|
||||||
|
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||||
|
const isUserAction = !!oldCode
|
||||||
|
fetchpostalRegion(newCode, false, isUserAction)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
postalRegion: readonly(postalRegion),
|
||||||
|
postalRegionOptions,
|
||||||
|
|
||||||
|
// State
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
error: readonly(error),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
fetchpostalRegion,
|
||||||
|
refreshpostalRegion,
|
||||||
|
getpostalRegionByCode,
|
||||||
|
clearCache,
|
||||||
|
clearAllCache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk direct access ke cached data (jika diperlukan)
|
||||||
|
export const usepostalRegionCache = () => ({
|
||||||
|
postalRegionCache: readonly(postalRegionCache),
|
||||||
|
loadingStates: readonly(loadingStates),
|
||||||
|
errorStates: readonly(errorStates),
|
||||||
|
})
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Province } from '~/models/province'
|
||||||
|
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { toTitleCase } from '~/lib/utils'
|
||||||
|
import * as provinceService from '~/services/province.service'
|
||||||
|
|
||||||
|
// Global state untuk caching
|
||||||
|
const provincesCache = ref<Province[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isInitialized = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
export function useProvinces() {
|
||||||
|
// Computed untuk format SelectItem
|
||||||
|
const provinceOptions = computed<SelectItem[]>(() => {
|
||||||
|
return provincesCache.value.map((province) => ({
|
||||||
|
label: toTitleCase(province.name),
|
||||||
|
value: province.code,
|
||||||
|
// code: province.code,
|
||||||
|
searchValue: `${province.code} ${province.name}`.trim(), // Untuk search internal combobox
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function untuk fetch data provinces
|
||||||
|
async function fetchProvinces(forceRefresh = false) {
|
||||||
|
// Jika sudah ada data dan tidak force refresh, skip
|
||||||
|
if (isInitialized.value && !forceRefresh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||||
|
if (isLoading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await provinceService.getList({
|
||||||
|
'page-no-limit': true,
|
||||||
|
sort: 'name:asc',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
provincesCache.value = response.body.data || []
|
||||||
|
isInitialized.value = true
|
||||||
|
} else {
|
||||||
|
error.value = 'Gagal memuat data provinsi'
|
||||||
|
console.error('Failed to fetch provinces:', response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Terjadi kesalahan saat memuat data provinsi'
|
||||||
|
console.error('Error fetching provinces:', err)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari province berdasarkan code
|
||||||
|
function getProvinceByCode(code: string): Province | undefined {
|
||||||
|
return provincesCache.value.find((province) => province.code === code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari province berdasarkan name
|
||||||
|
function getProvinceByName(name: string): Province | undefined {
|
||||||
|
return provincesCache.value.find((province) => province.name.toLowerCase() === name.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear cache (jika diperlukan)
|
||||||
|
function clearCache() {
|
||||||
|
provincesCache.value = []
|
||||||
|
isInitialized.value = false
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk refresh data
|
||||||
|
function refreshProvinces() {
|
||||||
|
return fetchProvinces(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto fetch saat composable pertama kali digunakan
|
||||||
|
if (!isInitialized.value && !isLoading.value) {
|
||||||
|
fetchProvinces()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
provinces: readonly(provincesCache),
|
||||||
|
provinceOptions,
|
||||||
|
|
||||||
|
// State
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
isInitialized: readonly(isInitialized),
|
||||||
|
error: readonly(error),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
fetchProvinces,
|
||||||
|
refreshProvinces,
|
||||||
|
getProvinceByCode,
|
||||||
|
getProvinceByName,
|
||||||
|
clearCache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk direct access ke cached data (jika diperlukan)
|
||||||
|
export const useProvincesCache = () => ({
|
||||||
|
provinces: readonly(provincesCache),
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
isInitialized: readonly(isInitialized),
|
||||||
|
})
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import type { Regency } from '~/models/regency'
|
||||||
|
import type { Item } from '~/components/pub/my-ui/combobox'
|
||||||
|
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||||
|
import { toTitleCase } from '~/lib/utils'
|
||||||
|
import * as regencyService from '~/services/regency.service'
|
||||||
|
|
||||||
|
// Interface untuk query parameters
|
||||||
|
interface RegencyQueryParams {
|
||||||
|
'province-code'?: string
|
||||||
|
search?: string
|
||||||
|
'page-number'?: number
|
||||||
|
'page-size'?: number
|
||||||
|
sort?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface untuk cached data dengan pagination
|
||||||
|
interface CachedRegencyData {
|
||||||
|
data: Regency[]
|
||||||
|
meta: PaginationMeta
|
||||||
|
queryKey: string // untuk tracking query yang berbeda
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global cache untuk regencies berdasarkan query key (province-code + search + pagination)
|
||||||
|
const regenciesCache = reactive(new Map<string, CachedRegencyData>())
|
||||||
|
const loadingStates = reactive(new Map<string, boolean>())
|
||||||
|
const errorStates = reactive(new Map<string, string | null>())
|
||||||
|
|
||||||
|
interface UseRegenciesOptions {
|
||||||
|
provinceCode?: Ref<string | undefined> | string | undefined
|
||||||
|
pageSize?: number
|
||||||
|
enablePagination?: boolean
|
||||||
|
enableSearch?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRegencies(options: UseRegenciesOptions | Ref<string | undefined> | string | undefined = {}) {
|
||||||
|
// Backward compatibility - jika parameter pertama adalah provinceCode
|
||||||
|
const normalizedOptions: UseRegenciesOptions =
|
||||||
|
typeof options === 'object' && 'value' in options
|
||||||
|
? { provinceCode: options }
|
||||||
|
: typeof options === 'string' || options === undefined
|
||||||
|
? { provinceCode: options }
|
||||||
|
: options
|
||||||
|
|
||||||
|
const { provinceCode, pageSize = 10, enablePagination = true, enableSearch = true } = normalizedOptions
|
||||||
|
|
||||||
|
// Convert provinceCode ke ref jika bukan ref
|
||||||
|
const provinceCodeRef =
|
||||||
|
typeof provinceCode === 'string' || provinceCode === undefined ? ref(provinceCode) : provinceCode || ref(undefined)
|
||||||
|
|
||||||
|
// State untuk pagination dan search
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const currentPageSize = ref(pageSize)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
// Function untuk generate query key
|
||||||
|
const generateQueryKey = (params: RegencyQueryParams) => {
|
||||||
|
const keyParts = [
|
||||||
|
params['province-code'] || '',
|
||||||
|
params.search || '',
|
||||||
|
params['page-number'] || 1,
|
||||||
|
params['page-size'] || pageSize,
|
||||||
|
params.sort || 'name:asc',
|
||||||
|
]
|
||||||
|
return keyParts.join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current query key
|
||||||
|
const currentQueryKey = computed(() => {
|
||||||
|
return generateQueryKey({
|
||||||
|
'province-code': provinceCodeRef.value,
|
||||||
|
search: enableSearch ? searchQuery.value : undefined,
|
||||||
|
'page-number': enablePagination ? currentPage.value : undefined,
|
||||||
|
'page-size': enablePagination ? currentPageSize.value : undefined,
|
||||||
|
sort: 'name:asc',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk mendapatkan regencies berdasarkan current query
|
||||||
|
const regencies = computed(() => {
|
||||||
|
const queryKey = currentQueryKey.value
|
||||||
|
const cachedData = regenciesCache.get(queryKey)
|
||||||
|
return cachedData?.data || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk pagination meta
|
||||||
|
const paginationMeta = computed(() => {
|
||||||
|
const queryKey = currentQueryKey.value
|
||||||
|
const cachedData = regenciesCache.get(queryKey)
|
||||||
|
return (
|
||||||
|
cachedData?.meta || {
|
||||||
|
recordCount: 0,
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: currentPageSize.value,
|
||||||
|
totalPage: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk loading state
|
||||||
|
const isLoading = computed(() => {
|
||||||
|
const queryKey = currentQueryKey.value
|
||||||
|
return loadingStates.get(queryKey) || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk error state
|
||||||
|
const error = computed(() => {
|
||||||
|
const queryKey = currentQueryKey.value
|
||||||
|
return errorStates.get(queryKey) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk format Item
|
||||||
|
const regencyOptions = computed<Item[]>(() => {
|
||||||
|
return regencies.value.map((regency) => ({
|
||||||
|
label: toTitleCase(regency.name),
|
||||||
|
value: regency.code,
|
||||||
|
searchValue: `${regency.code} ${regency.name}`.trim(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function untuk fetch regencies dengan pagination dan search
|
||||||
|
async function fetchRegencies(params?: Partial<RegencyQueryParams>, forceRefresh = false) {
|
||||||
|
const queryParams: RegencyQueryParams = {
|
||||||
|
'province-code': params?.['province-code'] || provinceCodeRef.value,
|
||||||
|
search: enableSearch ? params?.search || searchQuery.value : undefined,
|
||||||
|
'page-number': enablePagination ? params?.['page-number'] || currentPage.value : undefined,
|
||||||
|
'page-size': enablePagination ? params?.['page-size'] || currentPageSize.value : undefined,
|
||||||
|
sort: params?.sort || 'name:asc',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada province code, return
|
||||||
|
// if (!queryParams['province-code']) return // buat komponen select birth
|
||||||
|
|
||||||
|
const queryKey = generateQueryKey(queryParams)
|
||||||
|
|
||||||
|
// Jika tidak force refresh dan sudah ada cache, skip
|
||||||
|
if (!forceRefresh && regenciesCache.has(queryKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||||
|
if (loadingStates.get(queryKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingStates.set(queryKey, true)
|
||||||
|
errorStates.set(queryKey, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare API parameters
|
||||||
|
const apiParams: any = {
|
||||||
|
sort: queryParams.sort,
|
||||||
|
'province-code': queryParams['province-code'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pagination or no-limit based on enablePagination
|
||||||
|
if (enablePagination && queryParams['page-number'] && queryParams['page-size']) {
|
||||||
|
apiParams['page-number'] = queryParams['page-number']
|
||||||
|
apiParams['page-size'] = queryParams['page-size']
|
||||||
|
} else {
|
||||||
|
apiParams['page-no-limit'] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search if enabled and has value
|
||||||
|
if (enableSearch && queryParams.search && queryParams.search.trim().length >= 3) {
|
||||||
|
apiParams.search = queryParams.search.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await regencyService.getList(apiParams)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const regenciesData = response.body.data || []
|
||||||
|
const meta = response.body.meta || { record_totalCount: regenciesData.length }
|
||||||
|
|
||||||
|
// Create pagination meta
|
||||||
|
const paginationMeta: PaginationMeta = {
|
||||||
|
recordCount: meta.record_totalCount,
|
||||||
|
page: queryParams['page-number'] || 1,
|
||||||
|
pageSize: queryParams['page-size'] || regenciesData.length,
|
||||||
|
totalPage:
|
||||||
|
enablePagination && queryParams['page-size']
|
||||||
|
? Math.ceil(meta.record_totalCount / queryParams['page-size'])
|
||||||
|
: 1,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enablePagination && queryParams['page-size']) {
|
||||||
|
paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage
|
||||||
|
paginationMeta.hasPrev = paginationMeta.page > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
regenciesCache.set(queryKey, {
|
||||||
|
data: [...regenciesData],
|
||||||
|
meta: { ...paginationMeta },
|
||||||
|
queryKey,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errorStates.set(queryKey, 'Gagal memuat data kabupaten/kota')
|
||||||
|
console.error('Failed to fetch regencies:', response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorStates.set(queryKey, 'Terjadi kesalahan saat memuat data kabupaten/kota')
|
||||||
|
console.error('Error fetching regencies:', err)
|
||||||
|
} finally {
|
||||||
|
loadingStates.set(queryKey, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari regency berdasarkan code (search di semua cached data)
|
||||||
|
function getRegencyByCode(code: string): Regency | undefined {
|
||||||
|
// Search di semua cached data
|
||||||
|
for (const cachedData of regenciesCache.values()) {
|
||||||
|
const found = cachedData.data.find((regency) => regency.code === code)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari regency berdasarkan name (search di semua cached data)
|
||||||
|
function getRegencyByName(name: string): Regency | undefined {
|
||||||
|
// Search di semua cached data
|
||||||
|
for (const cachedData of regenciesCache.values()) {
|
||||||
|
const found = cachedData.data.find((regency) => regency.name.toLowerCase() === name.toLowerCase())
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk pagination
|
||||||
|
function goToPage(page: number) {
|
||||||
|
if (page >= 1 && page <= paginationMeta.value.totalPage) {
|
||||||
|
currentPage.value = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if (paginationMeta.value.hasNext) {
|
||||||
|
currentPage.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
if (paginationMeta.value.hasPrev) {
|
||||||
|
currentPage.value -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePageSize(size: number) {
|
||||||
|
currentPageSize.value = size
|
||||||
|
currentPage.value = 1 // Reset ke halaman pertama
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk search
|
||||||
|
function setSearch(query: string) {
|
||||||
|
searchQuery.value = query
|
||||||
|
currentPage.value = 1 // Reset ke halaman pertama saat search
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
searchQuery.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear cache berdasarkan query key pattern
|
||||||
|
function clearCache(provinceCodeParam?: string) {
|
||||||
|
const code = provinceCodeParam || provinceCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
// Clear semua cache yang mengandung province code tersebut
|
||||||
|
const keysToDelete: string[] = []
|
||||||
|
for (const [key] of regenciesCache.entries()) {
|
||||||
|
if (key.startsWith(code + '|')) {
|
||||||
|
keysToDelete.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToDelete.forEach((key) => {
|
||||||
|
regenciesCache.delete(key)
|
||||||
|
loadingStates.delete(key)
|
||||||
|
errorStates.delete(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear semua cache
|
||||||
|
function clearAllCache() {
|
||||||
|
regenciesCache.clear()
|
||||||
|
loadingStates.clear()
|
||||||
|
errorStates.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk refresh data
|
||||||
|
function refreshRegencies(params?: Partial<RegencyQueryParams>) {
|
||||||
|
return fetchRegencies(params, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced province code untuk mencegah multiple calls
|
||||||
|
const debouncedProvinceCode = refDebounced(provinceCodeRef, 100)
|
||||||
|
|
||||||
|
// Watch perubahan province code untuk auto fetch
|
||||||
|
watch(
|
||||||
|
debouncedProvinceCode,
|
||||||
|
(newCode, oldCode) => {
|
||||||
|
if (newCode && newCode !== oldCode) {
|
||||||
|
// Reset pagination dan search saat province code berubah
|
||||||
|
currentPage.value = 1
|
||||||
|
if (enableSearch) {
|
||||||
|
searchQuery.value = ''
|
||||||
|
}
|
||||||
|
fetchRegencies()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const triggerFetchAfterIdle = useDebounceFn(() => {
|
||||||
|
if (enableSearch) {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchRegencies()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// Watch perubahan search query untuk auto fetch dan reset halaman
|
||||||
|
watch(searchQuery, (newSearch, oldSearch) => {
|
||||||
|
if (newSearch !== oldSearch) {
|
||||||
|
triggerFetchAfterIdle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch perubahan pagination untuk auto fetch
|
||||||
|
watch([currentPage, currentPageSize], () => {
|
||||||
|
if (enablePagination) {
|
||||||
|
fetchRegencies()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(regencyOptions, (val) => {
|
||||||
|
console.log('[regencyOptions] updated', val.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
regencies: readonly(regencies),
|
||||||
|
regencyOptions,
|
||||||
|
|
||||||
|
// State
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
error: readonly(error),
|
||||||
|
paginationMeta: readonly(paginationMeta),
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
searchQuery: enableSearch ? searchQuery : readonly(ref('')),
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
currentPage: enablePagination ? currentPage : readonly(ref(1)),
|
||||||
|
currentPageSize: enablePagination ? currentPageSize : readonly(ref(pageSize)),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
fetchRegencies,
|
||||||
|
refreshRegencies,
|
||||||
|
getRegencyByCode,
|
||||||
|
getRegencyByName,
|
||||||
|
clearCache,
|
||||||
|
clearAllCache,
|
||||||
|
|
||||||
|
// Pagination methods
|
||||||
|
goToPage: enablePagination ? goToPage : () => {},
|
||||||
|
nextPage: enablePagination ? nextPage : () => {},
|
||||||
|
prevPage: enablePagination ? prevPage : () => {},
|
||||||
|
changePageSize: enablePagination ? changePageSize : () => {},
|
||||||
|
|
||||||
|
// Search methods
|
||||||
|
setSearch: enableSearch ? setSearch : () => {},
|
||||||
|
clearSearch: enableSearch ? clearSearch : () => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk direct access ke cached data (jika diperlukan)
|
||||||
|
export const useRegenciesCache = () => ({
|
||||||
|
regenciesCache: readonly(regenciesCache),
|
||||||
|
loadingStates: readonly(loadingStates),
|
||||||
|
errorStates: readonly(errorStates),
|
||||||
|
})
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
import type { Village } from '~/models/village'
|
||||||
|
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||||
|
import { toTitleCase } from '~/lib/utils'
|
||||||
|
import * as villageService from '~/services/village.service'
|
||||||
|
|
||||||
|
// Global cache untuk villages berdasarkan district code
|
||||||
|
const villagesCache = ref<Map<string, Village[]>>(new Map())
|
||||||
|
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||||
|
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||||
|
|
||||||
|
export function useVillages(districtCode: Ref<string | undefined> | string | undefined) {
|
||||||
|
// Convert districtCode ke ref jika bukan ref
|
||||||
|
const districtCodeRef =
|
||||||
|
typeof districtCode === 'string' || districtCode === undefined ? ref(districtCode) : districtCode
|
||||||
|
|
||||||
|
// Computed untuk mendapatkan villages berdasarkan district code
|
||||||
|
const villages = computed(() => {
|
||||||
|
const code = districtCodeRef.value
|
||||||
|
if (!code) return []
|
||||||
|
return villagesCache.value.get(code) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk loading state
|
||||||
|
const isLoading = computed(() => {
|
||||||
|
const code = districtCodeRef.value
|
||||||
|
if (!code) return false
|
||||||
|
return loadingStates.value.get(code) || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk error state
|
||||||
|
const error = computed(() => {
|
||||||
|
const code = districtCodeRef.value
|
||||||
|
if (!code) return null
|
||||||
|
return errorStates.value.get(code) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk format SelectItem
|
||||||
|
const villageOptions = computed<SelectItem[]>(() => {
|
||||||
|
return villages.value.map((village) => ({
|
||||||
|
label: toTitleCase(village.name),
|
||||||
|
value: village.code,
|
||||||
|
searchValue: `${village.code} ${village.name}`.trim(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function untuk fetch villages berdasarkan district code
|
||||||
|
async function fetchVillages(districtCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||||
|
const code = districtCodeParam || districtCodeRef.value
|
||||||
|
if (!code) return
|
||||||
|
|
||||||
|
// Jika user action atau force refresh, selalu fetch
|
||||||
|
// Jika bukan user action dan sudah ada cache, skip
|
||||||
|
if (!isUserAction && !forceRefresh && villagesCache.value.has(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||||
|
if (loadingStates.value.get(code)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahan: Cek apakah ada pending request untuk code yang sama
|
||||||
|
const pendingKey = `pending_${code}`
|
||||||
|
if (loadingStates.value.get(pendingKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingStates.value.set(pendingKey, true)
|
||||||
|
|
||||||
|
loadingStates.value.set(code, true)
|
||||||
|
errorStates.value.set(code, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await villageService.getList({
|
||||||
|
sort: 'name:asc',
|
||||||
|
'district-code': code,
|
||||||
|
'page-no-limit': true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const villagesData = response.body.data || []
|
||||||
|
villagesCache.value.set(code, villagesData)
|
||||||
|
} else {
|
||||||
|
errorStates.value.set(code, 'Gagal memuat data kelurahan')
|
||||||
|
console.error('Failed to fetch villages:', response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kelurahan')
|
||||||
|
console.error('Error fetching villages:', err)
|
||||||
|
} finally {
|
||||||
|
loadingStates.value.set(code, false)
|
||||||
|
loadingStates.value.delete(pendingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari village berdasarkan code
|
||||||
|
function getVillageByCode(code: string): Village | undefined {
|
||||||
|
const districtCode = districtCodeRef.value
|
||||||
|
if (!districtCode) return undefined
|
||||||
|
|
||||||
|
const villagesForDistrict = villagesCache.value.get(districtCode) || []
|
||||||
|
return villagesForDistrict.find((village) => village.code === code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk mencari village berdasarkan name
|
||||||
|
function getVillageByName(name: string): Village | undefined {
|
||||||
|
const districtCode = districtCodeRef.value
|
||||||
|
if (!districtCode) return undefined
|
||||||
|
|
||||||
|
const villagesForDistrict = villagesCache.value.get(districtCode) || []
|
||||||
|
return villagesForDistrict.find((village) => village.name.toLowerCase() === name.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear cache district tertentu
|
||||||
|
function clearCache(districtCodeParam?: string) {
|
||||||
|
const code = districtCodeParam || districtCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
villagesCache.value.delete(code)
|
||||||
|
loadingStates.value.delete(code)
|
||||||
|
errorStates.value.delete(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk clear semua cache
|
||||||
|
function clearAllCache() {
|
||||||
|
villagesCache.value.clear()
|
||||||
|
loadingStates.value.clear()
|
||||||
|
errorStates.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function untuk refresh data
|
||||||
|
function refreshVillages(districtCodeParam?: string) {
|
||||||
|
const code = districtCodeParam || districtCodeRef.value
|
||||||
|
if (code) {
|
||||||
|
return fetchVillages(code, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced district code untuk mencegah multiple calls
|
||||||
|
const debouncedDistrictCode = refDebounced(districtCodeRef, 100)
|
||||||
|
|
||||||
|
// Watch perubahan district code untuk auto fetch
|
||||||
|
watch(
|
||||||
|
debouncedDistrictCode,
|
||||||
|
(newCode, oldCode) => {
|
||||||
|
if (newCode && newCode !== oldCode) {
|
||||||
|
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||||
|
const isUserAction = !!oldCode
|
||||||
|
fetchVillages(newCode, false, isUserAction)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
villages: readonly(villages),
|
||||||
|
villageOptions,
|
||||||
|
|
||||||
|
// State
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
error: readonly(error),
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
fetchVillages,
|
||||||
|
refreshVillages,
|
||||||
|
getVillageByCode,
|
||||||
|
getVillageByName,
|
||||||
|
clearCache,
|
||||||
|
clearAllCache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export untuk direct access ke cached data (jika diperlukan)
|
||||||
|
export const useVillagesCache = () => ({
|
||||||
|
villagesCache: readonly(villagesCache),
|
||||||
|
loadingStates: readonly(loadingStates),
|
||||||
|
errorStates: readonly(errorStates),
|
||||||
|
})
|
||||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in New Issue
Block a user