feat(installation): add installation entry form and pages
- Create new installation entry form component with validation - Add installation list and add pages with basic routing - Implement custom select component for encounter class selection - Update SelectTrigger styling for better icon positioning
This commit is contained in:
@@ -1,57 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
// #region Props & Emits
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import Block from '~/components/pub/custom-ui/form/block.vue'
|
||||
import FieldGroup from '~/components/pub/custom-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/custom-ui/form/field.vue'
|
||||
import Label from '~/components/pub/custom-ui/form/label.vue'
|
||||
import Select from '~/components/pub/custom-ui/form/select.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
encounterClassCode: string
|
||||
modelValue: any
|
||||
encounterClassCode: {
|
||||
msg: {
|
||||
placeholder: string
|
||||
}
|
||||
items: {
|
||||
value: string
|
||||
label: string
|
||||
code: string
|
||||
}[]
|
||||
}
|
||||
errors?: FormErrors
|
||||
}>()
|
||||
const emit = defineEmits(['update:modelValue', 'event'])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update', value: string): void
|
||||
}>()
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
// =============================
|
||||
const count = ref(0)
|
||||
const double = computed(() => count.value * 2)
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
// init code
|
||||
fetchData()
|
||||
const data = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function fetchData() {
|
||||
console.log('fetched')
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch(count, (newVal, oldVal) => {
|
||||
console.log('count changed:', oldVal, '->', newVal)
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>Count: {{ count }}</p>
|
||||
<p>Double: {{ double }}</p>
|
||||
<button @click="increment">+1</button>
|
||||
</div>
|
||||
<form id="entry-form">
|
||||
<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>
|
||||
<FieldGroup :column="2">
|
||||
<Label>Nama</Label>
|
||||
<Field id="name" :errors="errors">
|
||||
<Input v-model="data.name" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup :column="2">
|
||||
<Label>Kode</Label>
|
||||
<Field id="code" :errors="errors">
|
||||
<Input v-model="data.code" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup :column="2">
|
||||
<Label height="compact">Encounter Code</Label>
|
||||
<Field id="encounterClassCode" :errors="errors">
|
||||
<Select v-model="data.encounterClassCode" :items="props.encounterClassCode.items" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* component style */
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import Action from '~/components/pub/custom-ui/nav-footer/ba-su.vue'
|
||||
|
||||
const { errors, setFromZodError, clearErrors } = useFormErrors()
|
||||
|
||||
const data = ref({
|
||||
code: '',
|
||||
name: '',
|
||||
encounterClassCode: '',
|
||||
})
|
||||
|
||||
const installation = {
|
||||
msg: {
|
||||
placeholder: '---pilih encounter class (fhir7)',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Ambulatory', code: 'AMB' },
|
||||
{ value: '2', label: 'Inpatient', code: 'IMP' },
|
||||
{ value: '3', label: 'Emergency', code: 'EMER' },
|
||||
{ value: '4', label: 'Observation', code: 'OBSENC' },
|
||||
{ value: '5', label: 'Pre-admission', code: 'PRENC' },
|
||||
{ value: '6', label: 'Short Stay', code: 'SS' },
|
||||
{ value: '7', label: 'Virtual', code: 'VR' },
|
||||
{ value: '8', label: 'Home Health', code: 'HH' },
|
||||
],
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Nama instalasi harus diisi'),
|
||||
code: z.string().min(1, 'Kode instalasi harus diisi'),
|
||||
encounterClassCode: z.string().min(1, 'Kelompok encounter class harus dipilih'),
|
||||
})
|
||||
|
||||
function onClick(type: string) {
|
||||
if (type === 'cancel') {
|
||||
navigateTo('/org-src/instalasion')
|
||||
} else if (type === 'draft') {
|
||||
// do something
|
||||
} else if (type === 'submit') {
|
||||
// Clear previous errors
|
||||
clearErrors()
|
||||
|
||||
const requestData = schema.safeParse(data.value)
|
||||
if (!requestData.success) {
|
||||
// Set errors menggunakan composable
|
||||
setFromZodError(requestData.error)
|
||||
|
||||
// Optional: tampilkan toast notification untuk error general
|
||||
console.warn('Form validation failed:', requestData.error)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Form data valid:', requestData.data)
|
||||
// do something with valid data
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-user" class="me-2" />
|
||||
<span class="font-semibold">Tambah</span> Instalasi
|
||||
</div>
|
||||
<div>
|
||||
<AppInstallationEntryForm v-model="data" :errors="errors" :encounter-class-code="installation" />
|
||||
</div>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="onClick" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectRoot } from 'radix-vue'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '~/components/pub/ui/select'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
label: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
items: Item[]
|
||||
placeholder?: string
|
||||
label?: string
|
||||
separator?: boolean
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// Sort items with selected item first, then alphabetically
|
||||
const sortedItems = computed(() => {
|
||||
const itemsWithSelection = props.items.map(item => ({
|
||||
...item,
|
||||
isSelected: item.value === props.modelValue,
|
||||
}))
|
||||
|
||||
return itemsWithSelection.sort((a, b) => {
|
||||
// Selected item always comes first
|
||||
if (a.isSelected && !b.isSelected) return -1
|
||||
if (!a.isSelected && b.isSelected) return 1
|
||||
|
||||
// If neither or both are selected, sort by label alphabetically
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
})
|
||||
|
||||
function onValueChange(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot :model-value="modelValue" @update:model-value="onValueChange">
|
||||
<SelectTrigger :class="cn('w-full', props.class)">
|
||||
<SelectValue :placeholder="placeholder || 'Pilih item'" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel v-if="label">
|
||||
{{ label }}
|
||||
</SelectLabel>
|
||||
|
||||
<SelectItem
|
||||
v-for="item in sortedItems"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.code" class="text-xs text-muted-foreground ml-2">
|
||||
{{ item.code }}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectSeparator v-if="separator" />
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
@@ -21,13 +21,13 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground flex h-10 w-full rounded-md border border-gray-400 px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground relative flex h-10 w-full rounded-md border border-gray-400 pl-3 pr-8 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<SelectIcon as-child class="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<Icon name="i-radix-icons-caret-sort" class="h-4 w-4 opacity-50" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
// import type { PagePermission } from '~/models/role'
|
||||
import Error from '~/components/pub/base/error/error.vue'
|
||||
// import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
// middleware: ['rbac'],
|
||||
// roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'Tambah Instalasi',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
// const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
|
||||
// const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// // Check if user has access to this page
|
||||
// const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
// const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
<FlowInstallationEntry />
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
// import type { PagePermission } from '~/models/role'
|
||||
import Error from '~/components/pub/base/error/error.vue'
|
||||
// import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
|
||||
definePageMeta({
|
||||
// middleware: ['rbac'],
|
||||
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||
title: 'List Division',
|
||||
contentFrame: 'cf-full-width',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: () => route.meta.title as string,
|
||||
})
|
||||
|
||||
// const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||
|
||||
// const { checkRole, hasReadAccess } = useRBAC()
|
||||
|
||||
// // Check if user has access to this page
|
||||
// const hasAccess = checkRole(roleAccess)
|
||||
// if (!hasAccess) {
|
||||
// navigateTo('/403')
|
||||
// }
|
||||
|
||||
// Define permission-based computed properties
|
||||
// const canRead = hasReadAccess(roleAccess)
|
||||
const canRead = true
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="canRead">
|
||||
route installation list
|
||||
</div>
|
||||
<Error v-else :status-code="403" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user