Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feat/integrasi-assessment-medis-114

This commit is contained in:
Abizrh
2025-10-20 20:09:39 +07:00
163 changed files with 5909 additions and 1662 deletions

No files matched your search

+1 -1
View File
@@ -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: {},
}
+36
View File
@@ -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>
+239 -241
View File
@@ -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>
-127
View File
@@ -1,127 +0,0 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-pdud.vue'))
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
export const config: Config = {
cols: [
{},
{},
{},
{ width: 100 },
{ width: 120 },
{},
{},
{},
{ width: 100 },
{ width: 100 },
{},
{ width: 50 },
],
headers: [
[
{ label: 'Nama' },
{ label: 'Rekam Medis' },
{ label: 'KTP' },
{ label: 'Tgl Lahir' },
{ label: 'Umur' },
{ label: 'JK' },
{ label: 'Pendidikan' },
{ label: 'Status' },
{ label: '' },
],
],
keys: [
'name',
'medicalRecord_number',
'identity_number',
'birth_date',
'patient_age',
'gender',
'education',
'status',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return `${recX.firstName} ${recX.middleName || ''} ${recX.lastName || ''}`
},
identity_number: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (recX.identity_number?.substring(0, 5) === 'BLANK') {
return '(TANPA NIK)'
}
return recX.identity_number
},
birth_date: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.birth_date == 'object' && recX.birth_date) {
return (recX.birth_date as Date).toLocaleDateString()
} else if (typeof recX.birth_date == 'string') {
return (recX.birth_date as string).substring(0, 10)
}
return recX.birth_date
},
patient_age: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.birth_date?.split('T')[0]
},
gender: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX?.gender_code !== 'number' && recX?.gender_code !== '') {
return 'Tidak Diketahui'
}
return recX.gender_code
},
education: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (typeof recX.education_code == 'number' && recX.education_code >= 0) {
return recX.education_code
} else if (typeof recX.education_code) {
return recX.education_code
}
return '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
status(rec, idx) {
const recX = rec as SmallDetailDto
if (recX.status_code === null) {
recX.status_code = 0
}
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
}
+104
View File
@@ -0,0 +1,104 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import type { Encounter } from '~/models/encounter'
import { educationCodes, genderCodes } from '~/lib/constants'
import { getAge } from '~/lib/date'
type SmallDetailDto = Encounter
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-pdud.vue'))
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
export const config: Config = {
cols: [
{},
{},
{},
{ width: 160 },
{},
{ width: 70 },
{ },
{ width: 50 },
],
headers: [
[
{ label: 'Nama' },
{ label: 'Rekam Medis' },
{ label: 'KTP' },
{ label: 'Tgl Lahir / Umur' },
{ label: 'JK' },
{ label: 'Pendidikan' },
{ label: 'Status', classVal: '!text-center' },
{ label: '' },
],
],
keys: [
'patient.person.name',
'patient.number',
'patient.person.residentIdentityNumber',
'birth_date',
'gender',
'education',
'status',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
gender: (rec: unknown): unknown => {
const recX = rec as Encounter
if (recX.patient?.person?.gender_code) {
return genderCodes[recX.patient.person.gender_code]
}
return '-'
},
education: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
if (recX.patient?.person?.education_code) {
return educationCodes[recX.patient.person.education_code]
}
return '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
status(rec, idx) {
const recX = rec as Encounter
if (!recX.status_code) {
recX.status_code = 'new'
}
const res: RecComponent = {
idx,
rec: recX,
component: statusBadge,
}
return res
},
},
htmls: {
birth_date: (rec: unknown): unknown => {
const recX = rec as Encounter
if (recX.patient?.person?.birthDate) {
return '' +
'<div>' + (recX.patient.person.birthDate as string).substring(0, 10) + ' / </div>' +
getAge(recX.patient.person.birthDate as string).extFormat
}
return '-'
},
},
}
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { config } from './list-cfg' import { config } from './list.cfg'
defineProps<{ const props = defineProps<{
data: any[] data: any[]
}>() }>()
</script> </script>
+34 -30
View File
@@ -1,35 +1,50 @@
<script setup lang="ts"> <script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'; import * as DE from '~/components/pub/my-ui/doc-entry';
import type { Encounter } from '~/models/encounter';
defineProps<{ const props = defineProps<{
data: any data: Encounter
}>() }>()
let address = ''
if (props.data.patient.person.addresses) {
address = props.data.patient.person.addresses.map(a => a.address).join(', ')
}
let dpjp = '';
if (props.data.responsible_doctor) {
const dp = props.data.responsible_doctor.employee.person
dpjp = `${dp.frontTitle} ${dp.name} ${dp.endTitle}`
} else if (props.data.appointment_doctor) {
dpjp = props.data.appointment_doctor.employee.person.name
}
</script> </script>
<template> <template>
<div class="w-full rounded-md border bg-white dark:bg-neutral-950 p-4 shadow-sm"> <div class="w-full rounded-md border bg-white dark:bg-neutral-950 p-4 shadow-sm">
<!-- Data Pasien --> <!-- Data Pasien -->
<h2 class="mb-2 md:text-base 2xl:text-lg font-semibold">{{ 'data.patient.person.name' }} - {{ 'data.patient.number' }}</h2> <h2 class="mb-2 md:text-base 2xl:text-lg font-semibold">{{ data.patient.person.name }} - {{ data.patient.number }}</h2>
<div class="grid grid-cols-3" > <div class="grid grid-cols-3" >
<div> <div>
<DE.Block mode="preview" labelSize="large"> <DE.Block mode="preview" labelSize="large">
<DE.Cell> <DE.Cell>
<DE.Label>Tgl. Lahir</DE.Label> <DE.Label class="font-semibold">No. RM</DE.Label>
<DE.Field> <DE.Field>
{{ 'data.patient.person.birthDate' }} {{ data.patient.person.birthDate?.substring(0, 10) }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
<DE.Cell> <DE.Cell>
<DE.Label>Jenis Kelamin</DE.Label> <DE.Label class="font-semibold">Jenis Kelamin</DE.Label>
<DE.Field> <DE.Field>
{{ 'data.patient.person.gender_code' }} {{ data.patient.person.gender_code }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
<DE.Cell> <DE.Cell>
<DE.Label>Alamat</DE.Label> <DE.Label class="font-semibold">Alamat</DE.Label>
<DE.Field> <DE.Field>
<div v-html="'data.patient.person .addresses[0].address'"></div> <div v-html="address"></div>
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
</DE.Block> </DE.Block>
@@ -37,21 +52,21 @@ defineProps<{
<div> <div>
<DE.Block mode="preview" labelSize="large"> <DE.Block mode="preview" labelSize="large">
<DE.Cell> <DE.Cell>
<DE.Label>Tgl. Kunjungan</DE.Label> <DE.Label class="font-semibold">Tgl. Kunjungan</DE.Label>
<DE.Field> <DE.Field>
{{ 'data.date' }} {{ data.visitDate.substring(0, 10) }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
<DE.Cell> <DE.Cell>
<DE.Label>Klinik</DE.Label> <DE.Label class="font-semibold">Klinik</DE.Label>
<DE.Field> <DE.Field>
{{ 'data.unit.name' }} {{ data.unit?.name }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
<DE.Cell> <DE.Cell>
<DE.Label>DPJP</DE.Label> <DE.Label class="font-semibold">DPJP</DE.Label>
<DE.Field> <DE.Field>
{{ 'data.doctor.name' }} {{ dpjp }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
</DE.Block> </DE.Block>
@@ -59,21 +74,10 @@ defineProps<{
<div> <div>
<DE.Block mode="preview" labelSize="large"> <DE.Block mode="preview" labelSize="large">
<DE.Cell> <DE.Cell>
<DE.Label>Tgl. Kunjungan</DE.Label> <DE.Label position="dynamic" class="!text-base 2xl:!text-lg font-semibold">Billing</DE.Label>
<DE.Field> <DE.Field class="text-base 2xl:text-lg">
{{ 'data.date' }} Rp. 000.000
</DE.Field> <!-- {{ data }} -->
</DE.Cell>
<DE.Cell>
<DE.Label>Klinik</DE.Label>
<DE.Field>
{{ 'data.unit.name' }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>DPJP</DE.Label>
<DE.Field>
{{ 'data.doctor.name' }}
</DE.Field> </DE.Field>
</DE.Cell> </DE.Cell>
</DE.Block> </DE.Block>
+12 -6
View File
@@ -1,22 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge' import { type Variants, Badge } from '~/components/pub/ui/badge'
import { dataStatusCodes } from '~/lib/constants';
const props = defineProps<{ const props = defineProps<{
rec: any rec: any
idx?: number idx?: number
}>() }>()
const doctorStatus = { const statusCodeColors: Record<string, Variants> = {
0: 'Tidak Aktif', new: 'warning',
1: 'Aktif', review: 'fresh',
process: 'fresh',
done: 'positive',
canceled: 'destructive',
rejected: 'destructive',
skiped: 'negative',
} }
const statusText = computed(() => { const statusText = computed(() => {
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus] return dataStatusCodes[props.rec.status_code as keyof typeof dataStatusCodes]
}) })
const badgeVariant = computed(() => { const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive' return (statusCodeColors[props.rec.status_code as keyof typeof statusCodeColors] || 'default')
}) })
</script> </script>
+106
View File
@@ -0,0 +1,106 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import Block from '~/components/pub/my-ui/form/block.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 { educationCodes, genderCodes, occupationCodes, religionCodes, relationshipCodes } from '~/lib/constants'
import { mapToComboboxOptList } from '~/lib/utils'
import { Form } from '~/components/pub/ui/form'
interface DivisionFormData {
name: string
code: string
parentId: string
}
const props = defineProps<{
division: {
msg: {
placeholder: string
search: string
empty: string
}
}
items: {
value: string
label: string
code: string
}[]
schema: any
initialValues?: Partial<DivisionFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: DivisionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const relationshipOpts = mapToComboboxOptList(relationshipCodes)
const educationOpts = mapToComboboxOptList(educationCodes)
const occupationOpts = mapToComboboxOptList(occupationCodes)
const genderOpts = mapToComboboxOptList(genderCodes)
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: DivisionFormData = {
name: values.name || '',
code: values.code || '',
parentId: values.parentId || '',
}
emit('submit', formData, resetForm)
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }"
as=""
keep-values
:validation-schema="formSchema"
:initial-values="initialValues"
>
<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">
<Block>
<!-- Specialist -->
<FieldGroup :column="2">
<Label label-for="specialist_id">Specialist</Label>
<Field id="specialist_id" :errors="errors">
<FormField v-slot="{ componentField }" name="specialist_id">
<FormItem>
<FormControl>
<Combobox id="specialist_id" v-bind="componentField" :items="genderOpts" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
<!-- Subpecialist -->
<FieldGroup :column="2">
<Label label-for="subspecialist_id">Subspecialist</Label>
<Field id="subspecialist_id" :errors="errors">
<FormField v-slot="{ componentField }" name="subspecialist_id">
<FormItem>
<FormControl>
<Combobox id="subspecialist_id" v-bind="componentField" :items="genderOpts" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</Field>
</FieldGroup>
</Block>
</div>
</div>
</form>
</Form>
</template>
@@ -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>
+3 -3
View File
@@ -85,7 +85,7 @@ function onCancelForm() {
<template> <template>
<form id="form-medicine" @submit.prevent> <form id="form-medicine" @submit.prevent>
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> <Block class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
<Cell> <Cell>
<Label height="">Kode</Label> <Label height="">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
@@ -111,7 +111,7 @@ function onCancelForm() {
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Kelompok Obat</Label> <Label >Kelompok Obat</Label>
<Field :errMessage="errors.medicineGroup_code"> <Field :errMessage="errors.medicineGroup_code">
<Select <Select
id="medicineGroup_code" id="medicineGroup_code"
@@ -139,7 +139,7 @@ function onCancelForm() {
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Satuan</Label> <Label>Satuan</Label>
<Field :errMessage="errors.uom_code"> <Field :errMessage="errors.uom_code">
<Select <Select
id="uom_code" id="uom_code"
@@ -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>
+162 -176
View File
@@ -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>
+27 -26
View File
@@ -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] || '-'
+96
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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>()
+87 -40
View File
@@ -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>
+119 -142
View File
@@ -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: {},
}
+36
View File
@@ -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>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+2 -3
View File
@@ -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>
+4 -4
View File
@@ -50,6 +50,7 @@ const refSearchNav: RefSearchNav = {
async function getPatientList() { async function getPatientList() {
isLoading.isTableLoading = true isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/patient') const resp = await xfetch('/api/v1/patient')
// const resp = await xfetch('/api/v1/encounter?includes=patient,patient-person')
if (resp.success) { if (resp.success) {
data.value = (resp.body as Record<string, any>).data data.value = (resp.body as Record<string, any>).data
} }
@@ -100,10 +101,10 @@ provide('table_data_loader', isLoading)
:ref-search-nav="refSearchNav" :ref-search-nav="refSearchNav"
/> />
<Separator class="my-4 xl:my-5" /> <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 <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
@@ -113,5 +114,4 @@ provide('table_data_loader', isLoading)
> >
<AppEncounterFilter /> <AppEncounterFilter />
</Dialog> </Dialog>
<!-- <Pagination :pagination-meta="paginationMeta" @page-change="handlePageChange" /> -->
</template> </template>
+11 -20
View File
@@ -3,17 +3,16 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
// Components
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import { getDetail } from '~/services/encounter.service' import { getDetail } from '~/services/encounter.service'
// Components
import type { Encounter } from '~/models/encounter'
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
import AssesmentFunctionList from '~/components/content/soapi/entry.vue' import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue' import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue' import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue' import PrescriptionList from '~/components/content/prescription/list.vue'
import type { Encounter } from '~/models/encounter'
import Status from '~/components/app/encounter/status.vue' import Status from '~/components/app/encounter/status.vue'
const route = useRoute() const route = useRoute()
@@ -28,23 +27,15 @@ const activeTab = computed({
}) })
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0 const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const encounter = ref<Encounter>((await getDetail(id)) as Encounter) const dataRes = await getDetail(id, {
includes:
const data = { 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
noRm: 'RM21123', })
nama: 'Ahmad Sutanto', const dataResBody = dataRes.body ?? null
alamat: 'Jl Jaksa Agung Suprapto No. 12, Jakarta', const data = dataResBody?.data ?? null
tanggalKunjungan: '23 April 2024',
klinik: 'Bedah',
tanggalLahir: '23 April 1990 (25 Tahun)',
jenisKelamin: 'Laki-laki',
jenisPembayaran: 'JKN',
noBilling: '223332',
dpjp: 'dr. Syaifullah, Sp.OT(K)',
}
const tabs: TabItem[] = [ const tabs: TabItem[] = [
{ value: 'status', label: 'Status Masuk/Keluar', component: Status }, { value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
{ value: 'early-medical-assessment', label: 'Pengkajian Awal Medis', component: EarlyMedicalAssesmentList }, { value: 'early-medical-assessment', label: 'Pengkajian Awal Medis', component: EarlyMedicalAssesmentList },
{ {
value: 'rehab-medical-assessment', value: 'rehab-medical-assessment',
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+2 -3
View File
@@ -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" />
+2 -3
View File
@@ -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"
+1 -1
View File
@@ -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"
+4 -7
View File
@@ -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>
+172 -43
View File
@@ -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>
+41 -36
View File
@@ -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"
+1 -1
View File
@@ -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">
+2 -3
View File
@@ -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>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+6 -8
View File
@@ -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"
+2 -3
View File
@@ -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>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -37,7 +37,7 @@ const openCollapsible = ref(false)
<SidebarMenuSubButton as-child> <SidebarMenuSubButton as-child>
<NuxtLink <NuxtLink
:to="subItem.link" :to="subItem.link"
class="mx-4 rounded-lg py-2 2xl:py-2.5 text-sm transition-all duration-200 md:!text-xs 2xl:!text-sm 2xl:!text-base" class="mx-4 rounded-lg py-2 2xl:py-2.5 text-sm transition-all duration-200 md:!text-xs 2xl:!text-sm"
active-class="bg-primary text-white" active-class="bg-primary text-white"
@click="setOpenMobile(false)" @click="setOpenMobile(false)"
> >
@@ -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>
+18 -13
View File
@@ -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',
) )
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { isRef } from 'vue';
import type { DataTableLoader } from '~/components/pub/my-ui/data/types' import type { DataTableLoader } from '~/components/pub/my-ui/data/types'
import type { Config } from './index' import type { Config } from './index'
import { Info } from 'lucide-vue-next' import { Info } from 'lucide-vue-next'
@@ -33,6 +34,24 @@ function toggleSelection(row: any) {
} }
} }
function deepFetch(data: Record<string, any>, key: string): string {
let result = '';
const keys = key.split('.')
let lastVal: any = isRef(data) ? {...(data.value as any)} : data
for (let i = 0; i < keys.length; i++) {
let idx = keys[i] || ''
if (typeof lastVal[idx] != undefined && lastVal[idx] != null) {
if (i == keys.length - 1) {
return lastVal[idx];
} else {
lastVal = isRef(lastVal[idx]) ? {...(lastVal[idx].value as any)} : lastVal[idx]
}
}
}
return result;
}
function handleActionCellClick(event: Event, _cellRef: string) { function handleActionCellClick(event: Event, _cellRef: string) {
// Prevent event if clicked directly on the button/dropdown // Prevent event if clicked directly on the button/dropdown
const target = event.target as HTMLElement const target = event.target as HTMLElement
@@ -52,14 +71,14 @@ function handleActionCellClick(event: Event, _cellRef: string) {
<template> <template>
<Table> <Table>
<TableHeader v-if="headers" class="bg-gray-50 dark:bg-gray-800"> <TableHeader v-if="headers" class="bg-gray-50 dark:bg-gray-800">
<TableRow> <TableRow v-for="(hr, hrIdx) in headers">
<TableHead <TableHead
v-for="(h, idx) in headers[0]" v-for="(th, idx) in headers[hrIdx]"
:key="`head-${idx}`" :key="`head-${idx}`"
class="border" :class="`border ${th.classVal || ''}`"
:style="{ width: cols[idx]?.width ? `${cols[idx].width}px` : undefined }" :style="{ width: cols[idx]?.width ? `${cols[idx].width}px` : undefined }"
> >
{{ h.label }} {{ th.label }}
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -116,7 +135,7 @@ function handleActionCellClick(event: Event, _cellRef: string) {
<template v-else> <template v-else>
<div v-if="htmls?.[key]" v-html="htmls?.[key]?.(row, rowIndex)"></div> <div v-if="htmls?.[key]" v-html="htmls?.[key]?.(row, rowIndex)"></div>
<template v-else> <template v-else>
{{ parses?.[key]?.(row, rowIndex) ?? (row as any)[key] }} {{ parses?.[key]?.(row, rowIndex) ?? deepFetch((row as any), key) }}
</template> </template>
</template> </template>
</TableCell> </TableCell>
@@ -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>
+5 -5
View File
@@ -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>
+1 -1
View File
@@ -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>
+9 -6
View File
@@ -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>
+1 -1
View File
@@ -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>
+4 -4
View File
@@ -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" />
+5 -5
View File
@@ -24,14 +24,14 @@ 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 size-4 align-middle md:size-6" class="mr-2 align-middle md:size-6"
/> />
{{ props.prep.title }} {{ props.prep.title }}
</div> </div>
@@ -44,7 +44,6 @@ function btnClick() {
<Input <Input
type="text" type="text"
placeholder="Search" placeholder="Search"
class="sm:text-sm"
@click="emitSearchNavClick" @click="emitSearchNavClick"
@input="onInput" @input="onInput"
/> />
@@ -59,7 +58,7 @@ function btnClick() {
> >
<Icon <Icon
name="i-lucide-plus" name="i-lucide-plus"
class="mr-2 h-4 w-4 align-middle" class="align-middle"
/> />
{{ prep.addNav.label }} {{ prep.addNav.label }}
</Button> </Button>
@@ -67,4 +66,5 @@ function btnClick() {
</div> </div>
</div> </div>
</header> </header>
<Separator class="my-4 xl:my-5" />
</template> </template>
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more