fix: solve conflict at material entry
This commit is contained in:
No files matched your search
@@ -20,16 +20,20 @@ RSSA - Front End
|
|||||||
|
|
||||||
- `app.vue`: Main layout
|
- `app.vue`: Main layout
|
||||||
- `components` : Contains all reusable UI components.
|
- `components` : Contains all reusable UI components.
|
||||||
- `components/flow` : Entry point for business logic and workflows. Pages or routes call these flow components to handle API requests and process application logic
|
- `components/content` : Entry point for business logic and workflows. Pages or routes call these content components to handle API requests and process application logic
|
||||||
- `components/app` : View-layer components that manage and present data. These are used within `flow/` to render or handle specific parts of the UI, and return results back to the flow
|
- `components/app` : View-layer components that manage and present data. These are used within `content/` to render or handle specific parts of the UI, and return results back to the content
|
||||||
- `components/pub` : Public/shared components used across different parts of the app.
|
- `components/pub` : Public/shared components used across different parts of the app.
|
||||||
- `composables` : Contains reusable logic and utility functions (e.g. composables, hooks)..
|
- `composables` : Contains reusable logic and utility functions (e.g. composables, hooks)..
|
||||||
- `layouts` : Reusable UI layout patterns used across pages.
|
- `layouts` : Reusable UI layout patterns used across pages.
|
||||||
|
- `models` : Contains data definitions or interfaces.
|
||||||
|
- `schemas` : Contains JSON schemas used for validation.
|
||||||
|
- `services` : Contains reusable API calls and business logic.
|
||||||
|
|
||||||
|
|
||||||
## Directory Structure for `app/pages`
|
## Directory Structure for `app/pages`
|
||||||
|
|
||||||
- `pages/auth` : Authentication related pages.
|
- `pages/auth` : Authentication related pages.
|
||||||
- `pages/(features)` : Grouped feature modules that reflect specific business flow or domains.
|
- `pages/(features)` : Grouped feature modules that reflect specific business content or domains.
|
||||||
|
|
||||||
## Directory Structure for `server/`
|
## Directory Structure for `server/`
|
||||||
|
|
||||||
@@ -50,16 +54,16 @@ The basic development workflow follows these steps:
|
|||||||
- Keep components pure, avoid making HTTP requests directly within them.
|
- Keep components pure, avoid making HTTP requests directly within them.
|
||||||
- They receive data via props and emit events upward.
|
- They receive data via props and emit events upward.
|
||||||
|
|
||||||
### Business Logic in `components/flow`
|
### Business Logic in `components/content`
|
||||||
|
|
||||||
- This layer connects the UI with the logic (API calls, validations, navigation).
|
- This layer connects the UI with the logic (API calls, validations, navigation).
|
||||||
- It composes components from `components/app/`, `components/pub/`, and other flow.
|
- It composes components from `components/app/`, `components/pub/`, and other content.
|
||||||
- Also responsible for managing state, side effects, and interactions.
|
- Also responsible for managing state, side effects, and interactions.
|
||||||
|
|
||||||
### Create Pages in `pages/`
|
### Create Pages in `pages/`
|
||||||
|
|
||||||
- Define permissions and guards for each page.
|
- Define permissions and guards for each page.
|
||||||
- Pages load the appropriate flow from `components/flow/`.
|
- Pages load the appropriate content from `components/content/`.
|
||||||
- They do not contain UI or logic directly, just route level layout or guards.
|
- They do not contain UI or logic directly, just route level layout or guards.
|
||||||
|
|
||||||
## Code Conventions
|
## Code Conventions
|
||||||
|
|||||||
@@ -327,4 +327,14 @@ body {
|
|||||||
.rounded-sm {
|
.rounded-sm {
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Form Error Styling */
|
||||||
|
.field-error-info {
|
||||||
|
@apply text-xs ml-1;
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
/* font-size: 0.875rem; */
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
/* .rounded-md { border-radius: var */
|
/* .rounded-md { border-radius: var */
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any; errors: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: 'item1', label: 'Item 1' },
|
||||||
|
{ value: 'item2', label: 'Item 2' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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="1">
|
||||||
|
<Label>Kode</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.code" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup v-if="!!props.errors.code">
|
||||||
|
<Label></Label>
|
||||||
|
<span class="text-red-400 text-sm">{{ props.errors.code }}</span>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="1">
|
||||||
|
<Label>Nama</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.name" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup v-if="!!props.errors.name">
|
||||||
|
<Label></Label>
|
||||||
|
<span class="text-red-400 text-sm">{{ props.errors.name }}</span>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="1">
|
||||||
|
<Label>Item</Label>
|
||||||
|
<Field>
|
||||||
|
<Select v-model="data.type" :items="items" placeholder="Pilih item" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="1">
|
||||||
|
<Label>Satuan</Label>
|
||||||
|
<Field>
|
||||||
|
<Select v-model="data.uom" :items="items" placeholder="Pilih item" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const cols: Col[] = [{ width: 100 }, { width: 250 }, { width: 100 }, { width: 100 }, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Item' }, { label: 'Satuan' }]]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'item_id', 'uom_code', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: Record<string, (row: any, ...args: any[]) => any> = {
|
||||||
|
name: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return `${recX.name}`.trim()
|
||||||
|
},
|
||||||
|
item_id: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return recX.item_id
|
||||||
|
},
|
||||||
|
uom_code: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return recX.uom_code
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: Record<string, (row: any, ...args: any[]) => any> = {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import Combobox from '~/components/pub/custom-ui/form/combobox.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'
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
function onCancelForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
emit('cancel', 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">
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="name">Nama</Label>
|
||||||
|
<Field id="name" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="name" type="text" placeholder="Masukkan nama divisi" autocomplete="off"
|
||||||
|
v-bind="componentField"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="code">Kode</Label>
|
||||||
|
<Field id="code" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="code">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="code" type="text" placeholder="Masukkan kode divisi" autocomplete="off" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label label-for="parentId">Kelompok</Label>
|
||||||
|
<Field id="parentId" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="parentId">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
id="parentId" v-bind="componentField" :items="props.division.items"
|
||||||
|
:placeholder="props.division.msg.placeholder" :search-placeholder="props.division.msg.search"
|
||||||
|
:empty-message="props.division.msg.empty"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue'))
|
||||||
|
|
||||||
|
export const cols: Col[] = [
|
||||||
|
{ width: 100 },
|
||||||
|
{ },
|
||||||
|
{ },
|
||||||
|
{ },
|
||||||
|
{ width: 50 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const header: Th[][] = [
|
||||||
|
[
|
||||||
|
{ label: 'Id' },
|
||||||
|
{ label: 'Nama' },
|
||||||
|
{ label: 'Kode' },
|
||||||
|
{ label: 'Kelompok' },
|
||||||
|
{ label: '' },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
export const keys = [
|
||||||
|
'id',
|
||||||
|
'firstName',
|
||||||
|
'cellphone',
|
||||||
|
'birth_place',
|
||||||
|
'action',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {
|
||||||
|
name: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return `${recX.frontTitle} ${recX.name} ${recX.endTitle}`.trim()
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
inPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.inPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
outPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.outPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
props: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } 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">
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="paginationMeta">
|
||||||
|
<div v-if="paginationMeta.totalPage > 1">
|
||||||
|
<PubCustomUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
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'
|
||||||
|
|
||||||
|
interface InstallationFormData {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
encounterClassCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
installation: {
|
||||||
|
msg: {
|
||||||
|
placeholder: string
|
||||||
|
}
|
||||||
|
items: {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
schema: any
|
||||||
|
initialValues?: Partial<InstallationFormData>
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'submit': [values: InstallationFormData, resetForm: () => void]
|
||||||
|
'cancel': [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
|
||||||
|
const formData: InstallationFormData = {
|
||||||
|
name: values.name || '',
|
||||||
|
code: values.code || '',
|
||||||
|
encounterClassCode: values.encounterClassCode || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
function onCancelForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
emit('cancel', 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">
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="name">Nama</Label>
|
||||||
|
<Field id="name" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="name" type="text" placeholder="Masukkan nama instalasi" autocomplete="off"
|
||||||
|
v-bind="componentField"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="code">Kode</Label>
|
||||||
|
<Field id="code" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="code">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="code" type="text" placeholder="Masukkan kode instalasi" autocomplete="off" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="parentId">Encounter Class</Label>
|
||||||
|
<Field id="encounterClassCode" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="encounterClassCode">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
v-bind="componentField"
|
||||||
|
:items="installation.items"
|
||||||
|
:placeholder="installation.msg.placeholder"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue'))
|
||||||
|
|
||||||
|
export const cols: Col[] = [{ width: 100 }, {}, {}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [
|
||||||
|
[{ label: 'Id' }, { label: 'Nama' }, { label: 'Kode' }, { label: 'Encounter Class' }, { label: '' }],
|
||||||
|
]
|
||||||
|
|
||||||
|
export const keys = ['id', 'name', 'cellphone', 'religion_code', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {
|
||||||
|
name: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return `${recX.firstName} ${recX.lastName || ''}`.trim()
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
inPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.inPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
outPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.outPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
props: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } 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">
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Data info and pagination -->
|
||||||
|
<template v-if="paginationMeta">
|
||||||
|
<div v-if="paginationMeta.totalPage > 1">
|
||||||
|
<PubCustomUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: '1', label: 'item 1' },
|
||||||
|
{ value: '2', label: 'item 2' },
|
||||||
|
{ value: '3', label: 'item 3' },
|
||||||
|
{ value: '4', label: 'item 4' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<Label>Items</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Perusahaan Insuransi</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Harga</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.price" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
const _doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cols: Col[] = [{}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '~/components/pub/ui/badge'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
return props.rec.status_code === 1 ? 'default' : 'destructive'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant">
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: '1', label: 'item 1' },
|
||||||
|
{ value: '2', label: 'item 2' },
|
||||||
|
{ value: '3', label: 'item 3' },
|
||||||
|
{ value: '4', label: 'item 4' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<Label>Nama</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.name" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Kode</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.code" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Item Group</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>UOM</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Infra</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Harga</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.price" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
const _doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cols: Col[] = [{}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '~/components/pub/ui/badge'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
return props.rec.status_code === 1 ? 'default' : 'destructive'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant">
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<Label>Nama</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.name" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Kode</Label>
|
||||||
|
<Field>
|
||||||
|
<Input />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
const _doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cols: Col[] = [{}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '~/components/pub/ui/badge'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
return props.rec.status_code === 1 ? 'default' : 'destructive'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant">
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<Label>Nama</Label>
|
||||||
|
<Field>
|
||||||
|
<Input v-model="data.name" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup>
|
||||||
|
<Label>Kode</Label>
|
||||||
|
<Field>
|
||||||
|
<Input />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
const _doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cols: Col[] = [{}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Aksi' }]]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emit = defineEmits(['update:modelValue', 'event'])
|
||||||
|
|
||||||
|
const data = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: '1', label: 'item 1' },
|
||||||
|
{ value: '2', label: 'item 2' },
|
||||||
|
{ value: '3', label: 'item 3' },
|
||||||
|
{ value: '4', label: 'item 4' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<Input type="text" name="identity_number" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Kode</Label>
|
||||||
|
<Field name="sip_number">
|
||||||
|
<Input type="text" name="sip_no" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Metode Pemberian</Label>
|
||||||
|
<Field name="phone">
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Bentuk Sediaan</Label>
|
||||||
|
<Field>
|
||||||
|
<Select :items="items" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Dosis</Label>
|
||||||
|
<Field>
|
||||||
|
<Input type="number" name="outPatient_rate" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Infra</Label>
|
||||||
|
<Field>
|
||||||
|
<Input />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Stock</Label>
|
||||||
|
<Field>
|
||||||
|
<Input type="number" />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
<FieldGroup :column="2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Field>
|
||||||
|
<Input />
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
|
||||||
|
|
||||||
|
export const cols: Col[] = [{}, {}, {}, {}, {}, {}, { width: 50 }]
|
||||||
|
|
||||||
|
export const header: Th[][] = [
|
||||||
|
[
|
||||||
|
{ label: 'Kode' },
|
||||||
|
{ label: 'Name' },
|
||||||
|
{ label: 'Kategori' },
|
||||||
|
{ label: 'Golongan' },
|
||||||
|
{ label: 'Metode Pemberian' },
|
||||||
|
{ label: 'Bentuk' },
|
||||||
|
{ label: 'Stok' },
|
||||||
|
{ label: 'Aksi' },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
export const keys = ['code', 'name', 'category', 'group', 'method', 'unit', 'total', 'action']
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {
|
||||||
|
cateogry: (rec: unknown): unknown => {
|
||||||
|
return (rec as SmallDetailDto).medicineCategory?.name || '-'
|
||||||
|
},
|
||||||
|
group: (rec: unknown): unknown => {
|
||||||
|
return (rec as SmallDetailDto).medicineGroup?.name || '-'
|
||||||
|
},
|
||||||
|
method: (rec: unknown): unknown => {
|
||||||
|
return (rec as SmallDetailDto).medicineMethod?.name || '-'
|
||||||
|
},
|
||||||
|
unit: (rec: unknown): unknown => {
|
||||||
|
return (rec as SmallDetailDto).medicineUnit?.name || '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action: (rec: unknown, idx: number): RecComponent => {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
// (_rec) {
|
||||||
|
// return '-'
|
||||||
|
// },
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: any[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data"
|
||||||
|
:cols="cols"
|
||||||
|
:header="header"
|
||||||
|
:keys="keys"
|
||||||
|
:func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml"
|
||||||
|
:func-component="funcComponent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '~/components/pub/ui/badge'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: any
|
||||||
|
idx?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const doctorStatus = {
|
||||||
|
0: 'Tidak Aktif',
|
||||||
|
1: 'Aktif',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeVariant = computed(() => {
|
||||||
|
return props.rec.status_code === 1 ? 'default' : 'destructive'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Badge :variant="badgeVariant">
|
||||||
|
{{ statusText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import Combobox from '~/components/pub/custom-ui/form/combobox.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'
|
||||||
|
|
||||||
|
interface UnitFormData {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
parentId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
unit: {
|
||||||
|
msg: {
|
||||||
|
placeholder: string
|
||||||
|
search: string
|
||||||
|
empty: string
|
||||||
|
}
|
||||||
|
items: {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
schema: any
|
||||||
|
initialValues?: Partial<UnitFormData>
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'submit': [values: UnitFormData, resetForm: () => void]
|
||||||
|
'cancel': [resetForm: () => void]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(props.schema)
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
|
||||||
|
const formData: UnitFormData = {
|
||||||
|
name: values.name || '',
|
||||||
|
code: values.code || '',
|
||||||
|
parentId: values.parentId || '',
|
||||||
|
}
|
||||||
|
emit('submit', formData, resetForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form cancel handler
|
||||||
|
function onCancelForm({ resetForm }: { resetForm: () => void }) {
|
||||||
|
emit('cancel', 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">
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="name">Nama</Label>
|
||||||
|
<Field id="name" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="name">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
id="name" type="text" placeholder="Masukkan nama unit" autocomplete="off"
|
||||||
|
v-bind="componentField"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="code">Kode</Label>
|
||||||
|
<Field id="code" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="code">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input id="code" type="text" placeholder="Masukkan kode unit" autocomplete="off" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
<Label label-for="parentId">Instalasi</Label>
|
||||||
|
<Field id="parentId" :errors="errors">
|
||||||
|
<FormField v-slot="{ componentField }" name="parentId">
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
id="parentId" v-bind="componentField" :items="unit.items"
|
||||||
|
:placeholder="unit.msg.placeholder" :search-placeholder="unit.msg.search"
|
||||||
|
:empty-message="unit.msg.empty"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<Button type="button" variant="outline" @click="onCancelForm({ resetForm })">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type {
|
||||||
|
Col,
|
||||||
|
KeyLabel,
|
||||||
|
RecComponent,
|
||||||
|
RecStrFuncComponent,
|
||||||
|
RecStrFuncUnknown,
|
||||||
|
Th,
|
||||||
|
} from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
type SmallDetailDto = any
|
||||||
|
|
||||||
|
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-ud.vue'))
|
||||||
|
|
||||||
|
export const cols: Col[] = [
|
||||||
|
{ width: 100 },
|
||||||
|
{ },
|
||||||
|
{ },
|
||||||
|
{ },
|
||||||
|
{ width: 50 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const header: Th[][] = [
|
||||||
|
[
|
||||||
|
{ label: 'Id' },
|
||||||
|
{ label: 'Nama' },
|
||||||
|
{ label: 'Kode' },
|
||||||
|
{ label: 'Instalasi' },
|
||||||
|
{ label: '' },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
export const keys = [
|
||||||
|
'id',
|
||||||
|
'firstName',
|
||||||
|
'cellphone',
|
||||||
|
'birth_place',
|
||||||
|
'action',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const delKeyNames: KeyLabel[] = [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const funcParsed: RecStrFuncUnknown = {
|
||||||
|
name: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return `${recX.frontTitle} ${recX.name} ${recX.endTitle}`.trim()
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
inPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.inPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
outPatient_itemPrice: (rec: unknown): unknown => {
|
||||||
|
const recX = rec as SmallDetailDto
|
||||||
|
return Number(recX.outPatient_itemPrice.price).toLocaleString('id-ID')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcComponent: RecStrFuncComponent = {
|
||||||
|
action(rec, idx) {
|
||||||
|
const res: RecComponent = {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
props: {
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const funcHtml: RecStrFuncUnknown = {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
|
||||||
|
import { cols, funcComponent, funcHtml, funcParsed, header, keys } 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">
|
||||||
|
<PubBaseDataTable
|
||||||
|
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
|
||||||
|
:func-html="funcHtml" :func-component="funcComponent" :skeleton-size="paginationMeta?.pageSize"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="paginationMeta">
|
||||||
|
<div v-if="paginationMeta.totalPage > 1">
|
||||||
|
<PubCustomUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Modal from '~/components/pub/base/modal/modal.vue'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const entry = ref<any>({})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
summary: false,
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const hreaderPrep: HeaderPrep = {
|
||||||
|
title: 'Golongan Obat',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => (isOpen.value = true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPatientList() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/medicine-group')
|
||||||
|
if (resp.success) {
|
||||||
|
data.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientList()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<AppMedicineGroupList :data="data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="lg" prevent-outside>
|
||||||
|
<AppMedicineGroupEntryForm v-model="entry" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Modal from '~/components/pub/base/modal/modal.vue'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const entry = ref<any>({})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
summary: false,
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const hreaderPrep: HeaderPrep = {
|
||||||
|
title: 'Item',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => (isOpen.value = true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPatientList() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/medicine-group')
|
||||||
|
if (resp.success) {
|
||||||
|
data.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientList()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<AppItemList :data="data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="xl" prevent-outside>
|
||||||
|
<AppItemEntryForm v-model="entry" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Modal from '~/components/pub/base/modal/modal.vue'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const entry = ref<any>({})
|
||||||
|
const page = ref(1)
|
||||||
|
const rowsPerPage = ref(10)
|
||||||
|
const totalPages = 20
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
summary: false,
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const hreaderPrep: HeaderPrep = {
|
||||||
|
title: 'Golongan Obat',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => (isOpen.value = true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPatientList() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/medicine-group')
|
||||||
|
if (resp.success) {
|
||||||
|
data.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientList()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<AppMedicineGroupList :data="data" />
|
||||||
|
<Pagination v-model:page="page" v-model:rows-per-page="rowsPerPage" :total-pages="totalPages" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model:open="isOpen" title="Tambah Golongan Obat" size="lg" prevent-outside>
|
||||||
|
<AppMedicineGroupEntryForm v-model="entry" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Modal from '~/components/pub/base/modal/modal.vue'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
import { getMedicineMethods } from '~/services/medicine-method.service'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const entry = ref<any>({})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
summary: false,
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const hreaderPrep: HeaderPrep = {
|
||||||
|
title: 'Metode Pemberian',
|
||||||
|
icon: 'i-lucide-medicine-bottle',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => (isOpen.value = true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
try {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
data.value = await getMedicineMethods()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<AppMedicineMethodList :data="data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model:open="isOpen" title="Tambah Metode Pemberian" size="lg" prevent-outside>
|
||||||
|
<AppMedicineMethodEntryForm v-model="entry" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const data = ref({
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
status: '',
|
||||||
|
type: '',
|
||||||
|
})
|
||||||
|
</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> Obat
|
||||||
|
</div>
|
||||||
|
<AppMedicineEntryForm v-model="data" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state management
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
summary: false,
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const hreaderPrep: HeaderPrep = {
|
||||||
|
title: 'Obat',
|
||||||
|
icon: 'i-lucide-medicine-bottle',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => navigateTo('/tools-equipment-src/medicine/add'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPatientList() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
const resp = await xfetch('/api/v1/medicine')
|
||||||
|
if (resp.success) {
|
||||||
|
data.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientList()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<AppMedicineList :data="data" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Action from '~/components/pub/custom-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { z, ZodError } from 'zod'
|
||||||
|
|
||||||
|
const errors = ref({})
|
||||||
|
const data = ref({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
code: z.string().min(1, 'Code must be at least 1 characters long'),
|
||||||
|
name: z.string().min(1, 'Name must be at least 1 characters long'),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function onClick(type: string) {
|
||||||
|
if (type === 'cancel') {
|
||||||
|
navigateTo('/tools-equipment-src/device')
|
||||||
|
} else if (type === 'draft') {
|
||||||
|
// do something
|
||||||
|
} else if (type === 'submit') {
|
||||||
|
// do something
|
||||||
|
const input = data.value
|
||||||
|
console.log(input)
|
||||||
|
const errorsParsed: any = {}
|
||||||
|
try {
|
||||||
|
const result = schema.safeParse(input)
|
||||||
|
if (!result.success) {
|
||||||
|
// You can handle the error here, e.g. show a message
|
||||||
|
const errorsCaptures = result?.error?.errors || []
|
||||||
|
const errorMessage = result.error.errors[0]?.message ?? 'Validation error occurred'
|
||||||
|
errorsCaptures.forEach((value: any) => {
|
||||||
|
const keyName = value?.path?.length > 0 ? value.path[0] : 'key'
|
||||||
|
errorsParsed[keyName as string] = value.message || ''
|
||||||
|
})
|
||||||
|
console.log(errorMessage)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ZodError) {
|
||||||
|
const jsonError = e.flatten()
|
||||||
|
console.log(JSON.stringify(jsonError, null, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
errors.value = errorsParsed
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||||
|
<Icon name="i-lucide-paint-bucket" class="me-2" />
|
||||||
|
<span class="font-semibold">Tambah</span> Alat Kesehatan
|
||||||
|
</div>
|
||||||
|
<AppDeviceEntryForm v-model="data" :errors="errors" />
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action @click="onClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// filter patient list
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// clear url param
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Alat Kesehatan',
|
||||||
|
icon: 'i-lucide-paint-bucket',
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah',
|
||||||
|
onClick: () => navigateTo('/tools-equipment-src/device/add'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMaterialList() {
|
||||||
|
isLoading.dataListLoading = true
|
||||||
|
|
||||||
|
const resp = await xfetch('/api/v1/device')
|
||||||
|
if (resp.success) {
|
||||||
|
data.value = (resp.body as Record<string, any>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.dataListLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getMaterialList()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<AppMaterialList v-if="!isLoading.dataListLoading" :data="data" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const division = {
|
||||||
|
msg: {
|
||||||
|
placeholder: '---pilih divisi utama',
|
||||||
|
search: 'kode, nama divisi',
|
||||||
|
empty: 'divisi tidak ditemukan',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ value: '1', label: 'Medical', code: 'MED' },
|
||||||
|
{ value: '2', label: 'Nursing', code: 'NUR' },
|
||||||
|
{ value: '3', label: 'Admin', code: 'ADM' },
|
||||||
|
{ value: '4', label: 'Support', code: 'SUP' },
|
||||||
|
{ value: '5', label: 'Education', code: 'EDU' },
|
||||||
|
{ value: '6', label: 'Pharmacy', code: 'PHA' },
|
||||||
|
{ value: '7', label: 'Radiology', code: 'RAD' },
|
||||||
|
{ value: '8', label: 'Laboratory', code: 'LAB' },
|
||||||
|
{ value: '9', label: 'Finance', code: 'FIN' },
|
||||||
|
{ value: '10', label: 'Human Resources', code: 'HR' },
|
||||||
|
{ value: '11', label: 'IT Services', code: 'ITS' },
|
||||||
|
{ value: '12', label: 'Maintenance', code: 'MNT' },
|
||||||
|
{ value: '13', label: 'Catering', code: 'CAT' },
|
||||||
|
{ value: '14', label: 'Security', code: 'SEC' },
|
||||||
|
{ value: '15', label: 'Emergency', code: 'EMR' },
|
||||||
|
{ value: '16', label: 'Surgery', code: 'SUR' },
|
||||||
|
{ value: '17', label: 'Outpatient', code: 'OUT' },
|
||||||
|
{ value: '18', label: 'Inpatient', code: 'INP' },
|
||||||
|
{ value: '19', label: 'Rehabilitation', code: 'REB' },
|
||||||
|
{ value: '20', label: 'Research', code: 'RSH' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schema = z.object({
|
||||||
|
name: z.string({
|
||||||
|
required_error: 'Nama wajib diisi',
|
||||||
|
}).min(1, 'Nama divisi wajib diisi'),
|
||||||
|
|
||||||
|
code: z.string({
|
||||||
|
required_error: 'Kode wajib diisi',
|
||||||
|
}).min(1, 'Kode divisi wajib diisi'),
|
||||||
|
|
||||||
|
parentId: z.preprocess(
|
||||||
|
(input: unknown) => {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
// Handle empty string case
|
||||||
|
if (input.trim() === '') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return Number(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
},
|
||||||
|
z.number({
|
||||||
|
required_error: 'Kelompok wajib dipilih',
|
||||||
|
}).min(1, 'Kelompok wajib dipilih'),
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import AppDivisonEntryForm from '~/components/app/divison/entry-form.vue'
|
||||||
|
import Dialog from '~/components/pub/base/modal/dialog.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { division as divisionConf, schema as schemaConf } from './entry'
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
// Dialog state
|
||||||
|
const isFormEntryDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
|
||||||
|
// Table action rowId provider
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
// Fungsi untuk fetch data division
|
||||||
|
async function fetchDivisionData(params: any) {
|
||||||
|
// Prepare query parameters for pagination and search
|
||||||
|
const urlParams = new URLSearchParams({
|
||||||
|
'page-number': params.page.toString(),
|
||||||
|
'page-size': params.pageSize.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (params.q) {
|
||||||
|
urlParams.append('search', params.q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await xfetch(`/api/v1/patient?${urlParams.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menggunakan composable untuk pagination
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getDivisionList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: fetchDivisionData,
|
||||||
|
entityName: 'division',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Divisi',
|
||||||
|
icon: 'i-lucide-box',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// Handle search input - this will be triggered by the header component
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
// Handle search button click if needed
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// Handle search clear
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah Divisi',
|
||||||
|
icon: 'i-lucide-send',
|
||||||
|
onClick: () => {
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
|
||||||
|
async function handleDeleteRow(record: any) {
|
||||||
|
try {
|
||||||
|
// TODO : hit backend request untuk delete
|
||||||
|
console.log('Deleting record:', record)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch(`/api/v1/division/${record.id}`, {
|
||||||
|
// method: 'DELETE'
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Refresh data setelah berhasil delete
|
||||||
|
await getDivisionList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Record deleted successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting record:', error)
|
||||||
|
// TODO: Show error message
|
||||||
|
} finally {
|
||||||
|
// Reset record state
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Form event handlers
|
||||||
|
|
||||||
|
function onCancelForm(resetForm: () => void) {
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmitForm(values: any, resetForm: () => void) {
|
||||||
|
let isSuccess = false
|
||||||
|
try {
|
||||||
|
// TODO: Implement form submission logic
|
||||||
|
console.log('Form submitted:', values)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch('/api/v1/division', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: JSON.stringify(values)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// If successful, mark as success and close dialog
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
isSuccess = true
|
||||||
|
|
||||||
|
// Refresh data after successful submission
|
||||||
|
await getDivisionList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Division created successfully')
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.warn('Error submitting form:', error)
|
||||||
|
isSuccess = false
|
||||||
|
// Don't close dialog or reset form on error
|
||||||
|
// TODO: Show error message to user
|
||||||
|
} finally {
|
||||||
|
if (isSuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
|
||||||
|
// Watch for row actions
|
||||||
|
watch(recId, () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
// TODO: Handle edit action
|
||||||
|
// isFormEntryDialogOpen.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
// Trigger confirmation modal open
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle confirmation result
|
||||||
|
function handleConfirmDelete(record: any, action: string) {
|
||||||
|
console.log('Confirmed action:', action, 'for record:', record)
|
||||||
|
handleDeleteRow(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
|
||||||
|
<AppDivisonList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
|
||||||
|
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Divisi" size="lg" prevent-outside>
|
||||||
|
<AppDivisonEntryForm
|
||||||
|
:division="divisionConf" :schema="schemaConf"
|
||||||
|
:initial-values="{ name: '', code: '', parentId: '' }" @submit="onSubmitForm" @cancel="onCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p><strong>ID:</strong> {{ record?.id }}</p>
|
||||||
|
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
|
||||||
|
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const installationConf = {
|
||||||
|
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' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schemaConf = z.object({
|
||||||
|
name: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Nama instalasi harus diisi',
|
||||||
|
})
|
||||||
|
.min(3, 'Nama instalasi minimal 3 karakter'),
|
||||||
|
|
||||||
|
code: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Kode instalasi harus diisi',
|
||||||
|
})
|
||||||
|
.min(3, 'Kode instalasi minimal 3 karakter'),
|
||||||
|
|
||||||
|
encounterClassCode: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Kelompok encounter class harus dipilih',
|
||||||
|
})
|
||||||
|
.min(1, 'Kelompok encounter class harus dipilih'),
|
||||||
|
})
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import AppInstallationEntryForm from '~/components/app/installation/entry-form.vue'
|
||||||
|
import Dialog from '~/components/pub/base/modal/dialog.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { installationConf, schemaConf } from './entry'
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
// Dialog state
|
||||||
|
const isFormEntryDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
|
||||||
|
// Table action rowId provider
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
// Fungsi untuk fetch data installation
|
||||||
|
async function fetchInstallationData(params: any) {
|
||||||
|
// Prepare query parameters for pagination and search
|
||||||
|
const urlParams = new URLSearchParams({
|
||||||
|
'page-number': params.page.toString(),
|
||||||
|
'page-size': params.pageSize.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (params.q) {
|
||||||
|
urlParams.append('search', params.q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await xfetch(`/api/v1/patient?${urlParams.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menggunakan composable untuk pagination
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getInstallationList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: fetchInstallationData,
|
||||||
|
entityName: 'installation',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Instalasi',
|
||||||
|
icon: 'i-lucide-box',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// Handle search input - this will be triggered by the header component
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
// Handle search button click if needed
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// Handle search clear
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah Instalasi',
|
||||||
|
icon: 'i-lucide-send',
|
||||||
|
onClick: () => {
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
|
||||||
|
async function handleDeleteRow(record: any) {
|
||||||
|
try {
|
||||||
|
// TODO : hit backend request untuk delete
|
||||||
|
console.log('Deleting record:', record)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch(`/api/v1/Installation/${record.id}`, {
|
||||||
|
// method: 'DELETE'
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Refresh data setelah berhasil delete
|
||||||
|
await getInstallationList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Record deleted successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting record:', error)
|
||||||
|
// TODO: Show error message
|
||||||
|
} finally {
|
||||||
|
// Reset record state
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Form event handlers
|
||||||
|
|
||||||
|
function onCancelForm(resetForm: () => void) {
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmitForm(values: any, resetForm: () => void) {
|
||||||
|
let isSuccess = false
|
||||||
|
try {
|
||||||
|
// TODO: Implement form submission logic
|
||||||
|
console.log('Form submitted:', values)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch('/api/v1/Installation', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: JSON.stringify(values)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// If successful, mark as success and close dialog
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
isSuccess = true
|
||||||
|
|
||||||
|
// Refresh data after successful submission
|
||||||
|
await getInstallationList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Installation created successfully')
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.warn('Error submitting form:', error)
|
||||||
|
isSuccess = false
|
||||||
|
// Don't close dialog or reset form on error
|
||||||
|
// TODO: Show error message to user
|
||||||
|
} finally {
|
||||||
|
if (isSuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
|
||||||
|
// Watch for row actions
|
||||||
|
watch(recId, () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
// TODO: Handle edit action
|
||||||
|
// isFormEntryDialogOpen.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
// Trigger confirmation modal open
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle confirmation result
|
||||||
|
function handleConfirmDelete(record: any, action: string) {
|
||||||
|
console.log('Confirmed action:', action, 'for record:', record)
|
||||||
|
handleDeleteRow(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
|
||||||
|
<AppInstallationList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
|
||||||
|
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Instalasi" size="lg" prevent-outside>
|
||||||
|
<AppInstallationEntryForm
|
||||||
|
:installation="installationConf" :schema="schemaConf"
|
||||||
|
:initial-values="{ name: '', code: '', encounterClassCode: '' }" @submit="onSubmitForm"
|
||||||
|
@cancel="onCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p><strong>ID:</strong> {{ record?.id }}</p>
|
||||||
|
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
|
||||||
|
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const unitConf = {
|
||||||
|
msg: {
|
||||||
|
placeholder: '--- pilih instalasi',
|
||||||
|
search: 'kode, nama instalasi',
|
||||||
|
empty: 'instalasi tidak ditemukan',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
|
||||||
|
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
|
||||||
|
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
|
||||||
|
{ value: '4', label: 'Instalasi Penunjang Non-Medis', code: 'SUP' },
|
||||||
|
{ value: '5', label: 'Instalasi Pendidikan & Pelatihan', code: 'EDU' },
|
||||||
|
{ value: '6', label: 'Instalasi Farmasi', code: 'PHA' },
|
||||||
|
{ value: '7', label: 'Instalasi Radiologi', code: 'RAD' },
|
||||||
|
{ value: '8', label: 'Instalasi Laboratorium', code: 'LAB' },
|
||||||
|
{ value: '9', label: 'Instalasi Keuangan', code: 'FIN' },
|
||||||
|
{ value: '10', label: 'Instalasi SDM', code: 'HR' },
|
||||||
|
{ value: '11', label: 'Instalasi Teknologi Informasi', code: 'ITS' },
|
||||||
|
{ value: '12', label: 'Instalasi Pemeliharaan & Sarana', code: 'MNT' },
|
||||||
|
{ value: '13', label: 'Instalasi Gizi / Catering', code: 'CAT' },
|
||||||
|
{ value: '14', label: 'Instalasi Keamanan', code: 'SEC' },
|
||||||
|
{ value: '15', label: 'Instalasi Gawat Darurat', code: 'EMR' },
|
||||||
|
{ value: '16', label: 'Instalasi Bedah Sentral', code: 'SUR' },
|
||||||
|
{ value: '17', label: 'Instalasi Rawat Jalan', code: 'OUT' },
|
||||||
|
{ value: '18', label: 'Instalasi Rawat Inap', code: 'INP' },
|
||||||
|
{ value: '19', label: 'Instalasi Rehabilitasi Medik', code: 'REB' },
|
||||||
|
{ value: '20', label: 'Instalasi Penelitian & Pengembangan', code: 'RSH' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schemaConf = z.object({
|
||||||
|
name: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Nama unit harus diisi',
|
||||||
|
})
|
||||||
|
.min(1, 'Nama unit harus diisi'),
|
||||||
|
|
||||||
|
code: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Kode unit harus diisi',
|
||||||
|
})
|
||||||
|
.min(1, 'Kode unit harus diisi'),
|
||||||
|
parentId: z.preprocess(
|
||||||
|
(input: unknown) => {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
// Handle empty string case
|
||||||
|
if (input.trim() === '') {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Number(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
},
|
||||||
|
z
|
||||||
|
.number({
|
||||||
|
required_error: 'Instalasi induk harus dipilih',
|
||||||
|
})
|
||||||
|
.refine((num) => num > 0, 'Instalasi induk harus dipilih'),
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import AppUnitEntryForm from '~/components/app/unit/entry-form.vue'
|
||||||
|
import Dialog from '~/components/pub/base/modal/dialog.vue'
|
||||||
|
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
import { schemaConf, unitConf } from './entry'
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
// Dialog state
|
||||||
|
const isFormEntryDialogOpen = ref(false)
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
|
||||||
|
// Table action rowId provider
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
// Fungsi untuk fetch data unit
|
||||||
|
async function fetchUnitData(params: any) {
|
||||||
|
// Prepare query parameters for pagination and search
|
||||||
|
const urlParams = new URLSearchParams({
|
||||||
|
'page-number': params.page.toString(),
|
||||||
|
'page-size': params.pageSize.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (params.q) {
|
||||||
|
urlParams.append('search', params.q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await xfetch(`/api/v1/patient?${urlParams.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menggunakan composable untuk pagination
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
fetchData: getUnitList,
|
||||||
|
} = usePaginatedList({
|
||||||
|
fetchFn: fetchUnitData,
|
||||||
|
entityName: 'unit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Unit',
|
||||||
|
icon: 'i-lucide-box',
|
||||||
|
refSearchNav: {
|
||||||
|
placeholder: 'Cari (min. 3 karakter)...',
|
||||||
|
minLength: 3,
|
||||||
|
debounceMs: 500,
|
||||||
|
showValidationFeedback: true,
|
||||||
|
onInput: (_val: string) => {
|
||||||
|
// Handle search input - this will be triggered by the header component
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
// Handle search button click if needed
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
// Handle search clear
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addNav: {
|
||||||
|
label: 'Tambah Unit',
|
||||||
|
icon: 'i-lucide-send',
|
||||||
|
onClick: () => {
|
||||||
|
isFormEntryDialogOpen.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
|
||||||
|
async function handleDeleteRow(record: any) {
|
||||||
|
try {
|
||||||
|
// TODO : hit backend request untuk delete
|
||||||
|
console.log('Deleting record:', record)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch(`/api/v1/unit/${record.id}`, {
|
||||||
|
// method: 'DELETE'
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Refresh data setelah berhasil delete
|
||||||
|
await getUnitList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Record deleted successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting record:', error)
|
||||||
|
// TODO: Show error message
|
||||||
|
} finally {
|
||||||
|
// Reset record state
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Form event handlers
|
||||||
|
|
||||||
|
function onCancelForm(resetForm: () => void) {
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmitForm(values: any, resetForm: () => void) {
|
||||||
|
let isSuccess = false
|
||||||
|
try {
|
||||||
|
// TODO: Implement form submission logic
|
||||||
|
console.log('Form submitted:', values)
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
// const response = await xfetch('/api/v1/unit', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: JSON.stringify(values)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// If successful, mark as success and close dialog
|
||||||
|
isFormEntryDialogOpen.value = false
|
||||||
|
isSuccess = true
|
||||||
|
|
||||||
|
// Refresh data after successful submission
|
||||||
|
await getUnitList()
|
||||||
|
|
||||||
|
// TODO: Show success message
|
||||||
|
console.log('Unit created successfully')
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.warn('Error submitting form:', error)
|
||||||
|
isSuccess = false
|
||||||
|
// Don't close dialog or reset form on error
|
||||||
|
// TODO: Show error message to user
|
||||||
|
} finally {
|
||||||
|
if (isSuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
resetForm()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
|
||||||
|
// Watch for row actions
|
||||||
|
watch(recId, () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
// TODO: Handle edit action
|
||||||
|
// isFormEntryDialogOpen.value = true
|
||||||
|
break
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
// Trigger confirmation modal open
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle confirmation result
|
||||||
|
function handleConfirmDelete(record: any, action: string) {
|
||||||
|
console.log('Confirmed action:', action, 'for record:', record)
|
||||||
|
handleDeleteRow(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-md border p-4">
|
||||||
|
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
|
||||||
|
<AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
|
||||||
|
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Unit" size="lg" prevent-outside>
|
||||||
|
<AppUnitEntryForm
|
||||||
|
:unit="unitConf" :schema="schemaConf" :initial-values="{ name: '', code: '', parentId: '' }"
|
||||||
|
@submit="onSubmitForm" @cancel="onCancelForm"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Record Confirmation Modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p><strong>ID:</strong> {{ record?.id }}</p>
|
||||||
|
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
|
||||||
|
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -1,19 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DataTableLoader } from './type'
|
import type { DataTableLoader } from './type'
|
||||||
|
import type { Col, RecStrFuncComponent, RecStrFuncUnknown, Th } from '~/components/pub/custom-ui/data/types'
|
||||||
import { Info } from 'lucide-vue-next'
|
import { Info } from 'lucide-vue-next'
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/pub/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/pub/ui/table'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
|
skeletonSize?: number
|
||||||
rows: unknown[]
|
rows: unknown[]
|
||||||
cols: any[]
|
cols: Col[]
|
||||||
header: any[]
|
header: Th[][]
|
||||||
keys: string[]
|
keys: string[]
|
||||||
funcParsed: Record<string, (row: any) => any>
|
funcParsed: RecStrFuncUnknown
|
||||||
funcHtml: Record<string, (row: any) => string>
|
funcHtml: RecStrFuncUnknown
|
||||||
funcComponent: Record<string, (row: any, idx: number) => any>
|
funcComponent: RecStrFuncComponent
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const getSkeletonSize = computed(() => {
|
||||||
|
return props.skeletonSize || 5
|
||||||
|
})
|
||||||
const loader = inject('table_data_loader') as DataTableLoader
|
const loader = inject('table_data_loader') as DataTableLoader
|
||||||
|
|
||||||
|
function handleActionCellClick(event: Event, _cellRef: string) {
|
||||||
|
// Prevent event if clicked directly on the button/dropdown
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (target.closest('button') || target.closest('[role="button"]')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the dropdown trigger button and click it
|
||||||
|
const cell = event.currentTarget as HTMLElement
|
||||||
|
const triggerButton = cell.querySelector('button[data-state]') || cell.querySelector('button')
|
||||||
|
if (triggerButton) {
|
||||||
|
(triggerButton as HTMLButtonElement).click()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,7 +51,7 @@ v-for="(h, idx) in header[0]" :key="`head-${idx}`" class="border"
|
|||||||
|
|
||||||
<TableBody v-if="loader.isTableLoading">
|
<TableBody v-if="loader.isTableLoading">
|
||||||
<!-- Loading state with 5 skeleton rows -->
|
<!-- Loading state with 5 skeleton rows -->
|
||||||
<TableRow v-for="n in 5" :key="`skeleton-${n}`">
|
<TableRow v-for="n in getSkeletonSize" :key="`skeleton-${n}`">
|
||||||
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-skel-${n}-${cellIndex}`" class="border">
|
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-skel-${n}-${cellIndex}`" class="border">
|
||||||
<Skeleton class="bg-gray-100 animate-pulse text-muted-foreground w-full h-6" />
|
<Skeleton class="bg-gray-100 animate-pulse text-muted-foreground w-full h-6" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -49,19 +69,29 @@ v-for="(h, idx) in header[0]" :key="`head-${idx}`" class="border"
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
<TableBody v-else>
|
<TableBody v-else>
|
||||||
<TableRow v-for="(row, rowIndex) in rows" :key="`row-${rowIndex}`">
|
<TableRow v-for="(row, rowIndex) in rows" :key="`row-${rowIndex}`">
|
||||||
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`" class="border">
|
<TableCell
|
||||||
|
v-for="(key, cellIndex) in keys"
|
||||||
|
:key="`cell-${rowIndex}-${cellIndex}`"
|
||||||
|
class="border"
|
||||||
|
:class="{ 'cursor-pointer': key === 'action' && funcComponent[key] }"
|
||||||
|
@click="key === 'action' && funcComponent[key] ? handleActionCellClick($event, `cell-${rowIndex}-${cellIndex}`) : null"
|
||||||
|
>
|
||||||
<!-- If funcComponent has a renderer -->
|
<!-- If funcComponent has a renderer -->
|
||||||
<component
|
<component
|
||||||
:is="funcComponent[key](row, rowIndex).component" v-if="funcComponent[key]"
|
:is="funcComponent[key]?.(row, rowIndex).component"
|
||||||
v-bind="funcComponent[key](row, rowIndex)"
|
v-if="funcComponent[key]"
|
||||||
/>
|
:ref="key === 'action' ? `actionComponent-${rowIndex}-${cellIndex}` : undefined"
|
||||||
|
:rec="row"
|
||||||
|
:idx="rowIndex"
|
||||||
|
v-bind="funcComponent[key]?.(row, rowIndex).props"
|
||||||
|
/>
|
||||||
<!-- If funcParsed or funcHtml returns a value -->
|
<!-- If funcParsed or funcHtml returns a value -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Use v-html for funcHtml to render HTML content -->
|
<!-- Use v-html for funcHtml to render HTML content -->
|
||||||
<div v-if="funcHtml[key]" v-html="funcHtml[key]?.(row)"></div>
|
<div v-if="funcHtml[key]" v-html="funcHtml[key]?.(row, rowIndex)"></div>
|
||||||
<!-- Use normal interpolation for funcParsed and regular data -->
|
<!-- Use normal interpolation for funcParsed and regular data -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ funcParsed[key]?.(row) ?? (row as any)[key] }}
|
{{ funcParsed[key]?.(row, rowIndex) ?? (row as any)[key] }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog } from '~/components/pub/ui/dialog'
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
preventOutside?: boolean
|
||||||
|
open?: boolean
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DialogProps>(), {
|
||||||
|
preventOutside: false,
|
||||||
|
open: false,
|
||||||
|
size: 'md',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Computed untuk menentukan class size berdasarkan prop size
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
const sizeMap = {
|
||||||
|
sm: 'sm:max-w-[350px]',
|
||||||
|
md: 'sm:max-w-[425px]',
|
||||||
|
lg: 'sm:max-w-[600px]',
|
||||||
|
xl: 'sm:max-w-[800px]',
|
||||||
|
full: 'sm:max-w-[95vw]',
|
||||||
|
}
|
||||||
|
return sizeMap[props.size]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed untuk state dialog
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<DialogContent
|
||||||
|
:class="sizeClass" @interact-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||||
|
@pointer-down-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ props.title }}</DialogTitle>
|
||||||
|
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<slot />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog } from '~/components/pub/ui/dialog'
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
preventOutside?: boolean
|
||||||
|
open?: boolean
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<DialogProps>(), {
|
||||||
|
preventOutside: false,
|
||||||
|
open: false,
|
||||||
|
size: 'md',
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
const sizeMap = {
|
||||||
|
sm: 'sm:max-w-[350px]',
|
||||||
|
md: 'sm:max-w-[425px]',
|
||||||
|
lg: 'sm:max-w-[600px]',
|
||||||
|
xl: 'sm:max-w-[800px]',
|
||||||
|
full: 'sm:max-w-[95vw]',
|
||||||
|
}
|
||||||
|
return sizeMap[props.size]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<DialogContent
|
||||||
|
:class="sizeClass"
|
||||||
|
@interact-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||||
|
@pointer-down-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ props.title }}</DialogTitle>
|
||||||
|
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<slot />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Confirmation Modal Components
|
||||||
|
|
||||||
|
Sistem confirmation modal yang modular dan dapat digunakan kembali di seluruh aplikasi.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. `confirmation.vue` - Base Confirmation Modal
|
||||||
|
|
||||||
|
Komponen dasar untuk modal konfirmasi yang dapat dikustomisasi.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `open?: boolean` - Status modal (terbuka/tertutup)
|
||||||
|
- `title?: string` - Judul modal (default: "Konfirmasi")
|
||||||
|
- `message?: string` - Pesan konfirmasi (default: "Apakah Anda yakin ingin melanjutkan?")
|
||||||
|
- `confirmText?: string` - Text tombol konfirmasi (default: "Ya")
|
||||||
|
- `cancelText?: string` - Text tombol batal (default: "Batal")
|
||||||
|
- `variant?: 'default' | 'destructive' | 'warning'` - Varian tampilan (default: "default")
|
||||||
|
- `size?: 'sm' | 'md' | 'lg' | 'xl'` - Ukuran modal (default: "md")
|
||||||
|
|
||||||
|
**Events:**
|
||||||
|
- `@confirm` - Dipanggil saat tombol konfirmasi diklik
|
||||||
|
- `@cancel` - Dipanggil saat tombol batal diklik
|
||||||
|
- `@update:open` - Dipanggil saat status modal berubah
|
||||||
|
|
||||||
|
**Contoh penggunaan:**
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isConfirmOpen"
|
||||||
|
title="Hapus Data"
|
||||||
|
message="Apakah Anda yakin ingin menghapus data ini?"
|
||||||
|
confirm-text="Hapus"
|
||||||
|
variant="destructive"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `record-confirmation.vue` - Record-specific Confirmation
|
||||||
|
|
||||||
|
Komponen khusus untuk konfirmasi operasi pada record/data tertentu.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `open?: boolean` - Status modal
|
||||||
|
- `action?: 'delete' | 'deactivate' | 'activate' | 'archive' | 'restore'` - Jenis aksi (default: "delete")
|
||||||
|
- `record?: RecordData | null` - Data record yang akan diproses
|
||||||
|
- `customTitle?: string` - Custom judul (opsional)
|
||||||
|
- `customMessage?: string` - Custom pesan (opsional)
|
||||||
|
- `customConfirmText?: string` - Custom text tombol konfirmasi (opsional)
|
||||||
|
- `customCancelText?: string` - Custom text tombol batal (opsional)
|
||||||
|
|
||||||
|
**Events:**
|
||||||
|
- `@confirm` - Dipanggil dengan parameter `(record, action)`
|
||||||
|
- `@cancel` - Dipanggil saat batal
|
||||||
|
- `@update:open` - Update status modal
|
||||||
|
|
||||||
|
**Contoh penggunaan:**
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmOpen"
|
||||||
|
action="delete"
|
||||||
|
:record="selectedRecord"
|
||||||
|
@confirm="handleDeleteRecord"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p><strong>ID:</strong> {{ record?.id }}</p>
|
||||||
|
<p><strong>Nama:</strong> {{ record?.name }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action Types
|
||||||
|
|
||||||
|
Record confirmation mendukung beberapa jenis aksi dengan konfigurasi default:
|
||||||
|
|
||||||
|
- **delete**: Hapus data (variant: destructive, warna merah)
|
||||||
|
- **deactivate**: Nonaktifkan data (variant: warning, warna kuning)
|
||||||
|
- **activate**: Aktifkan data (variant: default, warna biru)
|
||||||
|
- **archive**: Arsipkan data (variant: warning, warna kuning)
|
||||||
|
- **restore**: Pulihkan data (variant: default, warna biru)
|
||||||
|
|
||||||
|
## Integration Example
|
||||||
|
|
||||||
|
Contoh implementasi di komponen list:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
// State management
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const selectedRecord = ref(null)
|
||||||
|
const confirmAction = ref('delete')
|
||||||
|
|
||||||
|
// Handle action dari table
|
||||||
|
function handleRowAction(action, record) {
|
||||||
|
selectedRecord.value = record
|
||||||
|
confirmAction.value = action
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle konfirmasi
|
||||||
|
async function handleConfirmAction(record, action) {
|
||||||
|
try {
|
||||||
|
// API call berdasarkan action
|
||||||
|
await performAction(action, record.id)
|
||||||
|
// Refresh data
|
||||||
|
await refreshData()
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Your list component -->
|
||||||
|
<DataTable @action="handleRowAction" />
|
||||||
|
|
||||||
|
<!-- Confirmation modal -->
|
||||||
|
<RecordConfirmation
|
||||||
|
v-model:open="isRecordConfirmationOpen"
|
||||||
|
:action="confirmAction"
|
||||||
|
:record="selectedRecord"
|
||||||
|
@confirm="handleConfirmAction"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Komponen menggunakan Tailwind CSS dan shadcn/ui components. Pastikan dependencies berikut tersedia:
|
||||||
|
- `~/components/pub/custom-ui/modal/dialog.vue`
|
||||||
|
- `~/components/pub/ui/button`
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Dialog from '~/components/pub/base/modal/dialog.vue'
|
||||||
|
import { Button } from '~/components/pub/ui/button'
|
||||||
|
|
||||||
|
interface ConfirmationProps {
|
||||||
|
open?: boolean
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
confirmText?: string
|
||||||
|
cancelText?: string
|
||||||
|
variant?: 'default' | 'destructive' | 'warning'
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfirmationEmits {
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
'confirm': []
|
||||||
|
'cancel': []
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ConfirmationProps>(), {
|
||||||
|
open: false,
|
||||||
|
title: 'Konfirmasi',
|
||||||
|
message: 'Apakah Anda yakin ingin melanjutkan?',
|
||||||
|
confirmText: 'Ya',
|
||||||
|
cancelText: 'Batal',
|
||||||
|
variant: 'default',
|
||||||
|
size: 'md',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<ConfirmationEmits>()
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantClasses = computed(() => {
|
||||||
|
const variants = {
|
||||||
|
default: {
|
||||||
|
icon: 'i-lucide-help-circle',
|
||||||
|
iconColor: 'text-blue-500',
|
||||||
|
confirmVariant: 'default' as const,
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
icon: 'i-lucide-alert-triangle',
|
||||||
|
iconColor: 'text-red-500',
|
||||||
|
confirmVariant: 'destructive' as const,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
icon: 'i-lucide-alert-circle',
|
||||||
|
iconColor: 'text-yellow-500',
|
||||||
|
confirmVariant: 'default' as const,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return variants[props.variant]
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm')
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel')
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen" :title="title" :size="size">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Icon dan pesan -->
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slot untuk konten custom -->
|
||||||
|
<div v-if="$slots.default">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer buttons -->
|
||||||
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
|
<Button variant="outline" @click="handleCancel">
|
||||||
|
{{ cancelText }}
|
||||||
|
</Button>
|
||||||
|
<Button :variant="variantClasses.confirmVariant" @click="handleConfirm">
|
||||||
|
{{ confirmText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Confirmation from '~/components/pub/custom-ui/confirmation/confirmation.vue'
|
||||||
|
|
||||||
|
interface RecordData {
|
||||||
|
id: number | string
|
||||||
|
name?: string
|
||||||
|
title?: string
|
||||||
|
code?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecordConfirmationProps {
|
||||||
|
open?: boolean
|
||||||
|
action?: 'delete' | 'deactivate' | 'activate' | 'archive' | 'restore'
|
||||||
|
record?: RecordData | null
|
||||||
|
customTitle?: string
|
||||||
|
customMessage?: string
|
||||||
|
customConfirmText?: string
|
||||||
|
customCancelText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecordConfirmationEmits {
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
'confirm': [record: RecordData, action: string]
|
||||||
|
'cancel': []
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<RecordConfirmationProps>(), {
|
||||||
|
open: false,
|
||||||
|
action: 'delete',
|
||||||
|
record: null,
|
||||||
|
customTitle: '',
|
||||||
|
customMessage: '',
|
||||||
|
customConfirmText: '',
|
||||||
|
customCancelText: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<RecordConfirmationEmits>()
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (value) => emit('update:open', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const actionConfig = computed(() => {
|
||||||
|
const configs = {
|
||||||
|
delete: {
|
||||||
|
title: 'Hapus Data',
|
||||||
|
message: 'Apakah Anda yakin ingin menghapus data ini? Tindakan ini tidak dapat dibatalkan.',
|
||||||
|
confirmText: 'Hapus',
|
||||||
|
variant: 'destructive' as const,
|
||||||
|
},
|
||||||
|
deactivate: {
|
||||||
|
title: 'Nonaktifkan Data',
|
||||||
|
message: 'Apakah Anda yakin ingin menonaktifkan data ini?',
|
||||||
|
confirmText: 'Nonaktifkan',
|
||||||
|
variant: 'warning' as const,
|
||||||
|
},
|
||||||
|
activate: {
|
||||||
|
title: 'Aktifkan Data',
|
||||||
|
message: 'Apakah Anda yakin ingin mengaktifkan data ini?',
|
||||||
|
confirmText: 'Aktifkan',
|
||||||
|
variant: 'default' as const,
|
||||||
|
},
|
||||||
|
archive: {
|
||||||
|
title: 'Arsipkan Data',
|
||||||
|
message: 'Apakah Anda yakin ingin mengarsipkan data ini?',
|
||||||
|
confirmText: 'Arsipkan',
|
||||||
|
variant: 'warning' as const,
|
||||||
|
},
|
||||||
|
restore: {
|
||||||
|
title: 'Pulihkan Data',
|
||||||
|
message: 'Apakah Anda yakin ingin memulihkan data ini?',
|
||||||
|
confirmText: 'Pulihkan',
|
||||||
|
variant: 'default' as const,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return configs[props.action]
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalTitle = computed(() => {
|
||||||
|
return props.customTitle || actionConfig.value.title
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalMessage = computed(() => {
|
||||||
|
if (props.customMessage) {
|
||||||
|
return props.customMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseMessage = actionConfig.value.message
|
||||||
|
// if (props.record) {
|
||||||
|
// const recordName = props.record.name || props.record.title || props.record.code || `ID: ${props.record.id}`
|
||||||
|
// return `${baseMessage}\n\nData: ${recordName}`
|
||||||
|
// }
|
||||||
|
|
||||||
|
return baseMessage
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalConfirmText = computed(() => {
|
||||||
|
return props.customConfirmText || actionConfig.value.confirmText
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalCancelText = computed(() => {
|
||||||
|
return props.customCancelText || 'Batal'
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
if (props.record) {
|
||||||
|
emit('confirm', props.record, props.action)
|
||||||
|
}
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel')
|
||||||
|
emit('update:open', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isOpen" :title="finalTitle" :message="finalMessage" :confirm-text="finalConfirmText"
|
||||||
|
:cancel-text="finalCancelText" :variant="actionConfig.variant" size="md" @confirm="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<!-- Slot untuk informasi tambahan record -->
|
||||||
|
<div v-if="record && $slots.default" class="mt-4 p-3 bg-muted rounded-md">
|
||||||
|
<slot :record="record" :action="action" />
|
||||||
|
</div>
|
||||||
|
</Confirmation>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LinkItem, ListItemDto } from './types'
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rec: ListItemDto
|
rec: ListItemDto
|
||||||
@@ -11,13 +12,13 @@ const recItem = inject<Ref<any>>('rec_item')!
|
|||||||
|
|
||||||
function detail() {
|
function detail() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showDetail'
|
recAction.value = ActionEvents.showDetail
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showEdit'
|
recAction.value = ActionEvents.showEdit
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LinkItem, ListItemDto } from './types'
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rec: ListItemDto
|
rec: ListItemDto
|
||||||
@@ -11,19 +12,19 @@ const recItem = inject<Ref<any>>('rec_item')!
|
|||||||
|
|
||||||
function detail() {
|
function detail() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showDetail'
|
recAction.value = ActionEvents.showDetail
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showEdit'
|
recAction.value = ActionEvents.showEdit
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function del() {
|
function del() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showConfirmDel'
|
recAction.value = ActionEvents.showConfirmDelete
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LinkItem, ListItemDto } from './types'
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rec: ListItemDto
|
rec: ListItemDto
|
||||||
@@ -11,19 +12,19 @@ const recItem = inject<Ref<any>>('rec_item')!
|
|||||||
|
|
||||||
function detail() {
|
function detail() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showDetail'
|
recAction.value = ActionEvents.showDetail
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showEdit'
|
recAction.value = ActionEvents.showEdit
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function del() {
|
function del() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showConfirmDel'
|
recAction.value = ActionEvents.showConfirmDelete
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LinkItem, ListItemDto } from './types'
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
const props = defineProps<{
|
interface Props {
|
||||||
rec: ListItemDto
|
rec: ListItemDto
|
||||||
}>()
|
size?: 'default' | 'sm' | 'lg'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'lg',
|
||||||
|
})
|
||||||
|
|
||||||
const recId = inject<Ref<number>>('rec_id')!
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
const recAction = inject<Ref<string>>('rec_action')!
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
@@ -11,13 +17,13 @@ const recItem = inject<Ref<any>>('rec_item')!
|
|||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showEdit'
|
recAction.value = ActionEvents.showEdit
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function del() {
|
function del() {
|
||||||
recId.value = props.rec.id || 0
|
recId.value = props.rec.id || 0
|
||||||
recAction.value = 'showConfirmDel'
|
recAction.value = ActionEvents.showConfirmDelete
|
||||||
recItem.value = props.rec
|
recItem.value = props.rec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +50,7 @@ const linkItems: LinkItem[] = [
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
:size="size"
|
||||||
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white"
|
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white"
|
||||||
>
|
>
|
||||||
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
|
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface Th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonNav {
|
export interface ButtonNav {
|
||||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'negative' | 'ghost' | 'link'
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||||
classVal?: string
|
classVal?: string
|
||||||
classValExt?: string
|
classValExt?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@@ -56,6 +56,12 @@ export interface QuickSearchNav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RefSearchNav {
|
export interface RefSearchNav {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
minLength?: number
|
||||||
|
debounceMs?: number
|
||||||
|
inputClass?: string
|
||||||
|
showValidationFeedback?: boolean
|
||||||
onInput: (val: string) => void
|
onInput: (val: string) => void
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onClear: () => void
|
onClear: () => void
|
||||||
@@ -94,3 +100,9 @@ export interface LinkItem {
|
|||||||
onClick?: (event: Event) => void
|
onClick?: (event: Event) => void
|
||||||
headerStatus?: boolean
|
headerStatus?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ActionEvents = {
|
||||||
|
showConfirmDelete: 'showConfirmDel',
|
||||||
|
showEdit: 'showEdit',
|
||||||
|
showDetail: 'showDetail',
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: string
|
||||||
|
modelValue?: string
|
||||||
|
items: Item[]
|
||||||
|
placeholder?: string
|
||||||
|
searchPlaceholder?: string
|
||||||
|
emptyMessage?: string
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
const selectedItem = computed(() =>
|
||||||
|
props.items.find(item => item.value === props.modelValue),
|
||||||
|
)
|
||||||
|
|
||||||
|
const displayText = computed(() =>
|
||||||
|
selectedItem.value?.label || props.placeholder || '---pilih item',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create searchable items with combined code and label for better search
|
||||||
|
// Sort by:
|
||||||
|
// 1. Selected item first (highest priority)
|
||||||
|
// 2. Then by label alphabetically
|
||||||
|
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) => {
|
||||||
|
// 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 onSelect(item: Item) {
|
||||||
|
emit('update:modelValue', item.value)
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Popover v-model:open="open">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button
|
||||||
|
:id="props.id"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
:aria-expanded="open"
|
||||||
|
:aria-controls="`${props.id}-list`"
|
||||||
|
:aria-describedby="`${props.id}-search`"
|
||||||
|
:class="cn(
|
||||||
|
'w-full justify-between border-black bg-white hover:bg-gray-50 text-sm font-normal',
|
||||||
|
!modelValue && 'text-muted-foreground',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
{{ displayText }}
|
||||||
|
<Icon name="i-lucide-chevrons-up-down" class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
:id="`${props.id}-search`"
|
||||||
|
class="h-9"
|
||||||
|
:placeholder="searchPlaceholder || 'Cari...'"
|
||||||
|
:aria-label="`Cari ${displayText}`"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
|
||||||
|
<CommandList :id="`${props.id}-list`" role="listbox">
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem v-for="item in searchableItems" :key="item.value" :value="item.searchValue" @select="onSelect(item)">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<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',
|
||||||
|
modelValue === item.value ? 'opacity-100' : 'opacity-0',
|
||||||
|
)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export interface XError {
|
import type { XErrors } from '~/types/error'
|
||||||
message: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
id?: string
|
id?: string
|
||||||
errors?: Record<string, XError>
|
errors?: XErrors
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -14,7 +11,7 @@ defineProps<{
|
|||||||
<div class="grow">
|
<div class="grow">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="id && errors?.[id]" class="field-error-info">
|
<div v-if="id && errors?.[id]" class="field-error-info">
|
||||||
{{ errors[id].message }}
|
{{ errors[id]?.message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
labelFor: string
|
||||||
size?: 'default' | 'narrow' | 'wide'
|
size?: 'default' | 'narrow' | 'wide'
|
||||||
height?: 'default' | 'compact'
|
height?: 'default' | 'compact'
|
||||||
position?: 'default' | 'dynamic'
|
position?: 'default' | 'dynamic'
|
||||||
@@ -45,7 +46,7 @@ const labelClass = computed(() => positionChildMap[props.position])
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<label :class="labelClass">
|
<label :class="labelClass" :for="labelFor">
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,18 +1,142 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
|
import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
|
||||||
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
prep: HeaderPrep
|
prep: HeaderPrep
|
||||||
refSearchNav: RefSearchNav
|
modelValue?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
'search': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Internal search state
|
||||||
|
const searchInput = ref(props.modelValue || '')
|
||||||
|
const debouncedSearch = refDebounced(searchInput, props.prep.refSearchNav?.debounceMs || 500)
|
||||||
|
|
||||||
|
// Computed search model for v-model
|
||||||
|
const searchModel = computed({
|
||||||
|
get: () => searchInput.value,
|
||||||
|
set: (value: string) => {
|
||||||
|
searchInput.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue !== searchInput.value) {
|
||||||
|
searchInput.value = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch debounced search and emit search event
|
||||||
|
watch(debouncedSearch, (newValue) => {
|
||||||
|
const minLength = props.prep.refSearchNav?.minLength || 3
|
||||||
|
// Only search if meets minimum length or empty (to clear search)
|
||||||
|
if (newValue.length === 0 || newValue.length >= minLength) {
|
||||||
|
emit('search', newValue)
|
||||||
|
props.prep.refSearchNav?.onInput(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle clear search
|
||||||
|
function clearSearch() {
|
||||||
|
searchModel.value = ''
|
||||||
|
props.prep.refSearchNav?.onClear()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center justify-between">
|
||||||
<div class="ml-3 text-lg font-bold text-gray-900">
|
<div class="flex items-center">
|
||||||
<Icon :name="props.prep.icon!" class="mr-2 h-4 w-4 align-middle" />
|
<div class="ml-3 text-lg font-bold text-gray-900">
|
||||||
{{ props.prep.title }}
|
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
|
||||||
|
{{ props.prep.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<!-- Search Section -->
|
||||||
|
<div v-if="props.prep.refSearchNav" class="ml-3 text-lg text-gray-900 relative">
|
||||||
|
<div class="relative">
|
||||||
|
<Input
|
||||||
|
id="search-table"
|
||||||
|
v-model="searchModel"
|
||||||
|
name="search-table"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm"
|
||||||
|
:class="[
|
||||||
|
props.prep.refSearchNav.inputClass,
|
||||||
|
{
|
||||||
|
'border-amber-300 bg-amber-50': searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3),
|
||||||
|
'border-green-300 bg-green-50': searchInput.length >= (props.prep.refSearchNav.minLength || 3),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:placeholder="props.prep.refSearchNav.placeholder || 'Cari (min. 3 karakter)...'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Clear button -->
|
||||||
|
<button
|
||||||
|
v-if="searchInput.length > 0"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
type="button"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<Icon name="i-lucide-x" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Validation feedback -->
|
||||||
|
<div
|
||||||
|
v-if="props.prep.refSearchNav.showValidationFeedback !== false && searchInput.length > 0 && searchInput.length < (props.prep.refSearchNav.minLength || 3)"
|
||||||
|
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Minimal {{ props.prep.refSearchNav.minLength || 3 }} karakter untuk mencari
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Button -->
|
||||||
|
<div v-if="props.prep.addNav" class="m-2 flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
||||||
|
:class="props.prep.addNav.classVal"
|
||||||
|
:variant="(props.prep.addNav.variant as any) || 'default'"
|
||||||
|
@click="props.prep.addNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="props.prep.addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ props.prep.addNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Button -->
|
||||||
|
<div v-if="props.prep.filterNav" class="m-2 flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="props.prep.filterNav.classVal"
|
||||||
|
:variant="(props.prep.filterNav.variant as any) || 'default'"
|
||||||
|
@click="props.prep.filterNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="props.prep.filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ props.prep.filterNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Button -->
|
||||||
|
<div v-if="props.prep.printNav" class="m-2 flex items-center">
|
||||||
|
<Button
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||||
|
:class="props.prep.printNav.classVal"
|
||||||
|
:variant="(props.prep.printNav.variant as any) || 'default'"
|
||||||
|
@click="props.prep.printNav?.onClick"
|
||||||
|
>
|
||||||
|
<Icon :name="props.prep.printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
||||||
|
{{ props.prep.printNav.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface PaginationMeta {
|
||||||
|
recordCount: number
|
||||||
|
// page : current pointer for viewing data
|
||||||
|
page: number
|
||||||
|
// pageSize: limit each page request
|
||||||
|
pageSize: number
|
||||||
|
// totalPage: recourdCount / pageSize
|
||||||
|
totalPage: number
|
||||||
|
// hasNext: check if there is next page
|
||||||
|
hasNext: boolean
|
||||||
|
// hasPrev: check if there is previous page
|
||||||
|
hasPrev: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaginationMeta } from './pagination.type'
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationFirst,
|
||||||
|
PaginationLast,
|
||||||
|
PaginationList,
|
||||||
|
PaginationListItem,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrev,
|
||||||
|
} from '~/components/pub/ui/pagination'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
paginationMeta: PaginationMeta
|
||||||
|
onPageChange?: (page: number) => void
|
||||||
|
showInfo?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
onPageChange: undefined,
|
||||||
|
showInfo: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pageChange: [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
if (props.onPageChange) {
|
||||||
|
props.onPageChange(page)
|
||||||
|
}
|
||||||
|
emit('pageChange', page)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFirst() {
|
||||||
|
if (props.paginationMeta.hasPrev) {
|
||||||
|
handlePageChange(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
if (props.paginationMeta.hasPrev) {
|
||||||
|
handlePageChange(props.paginationMeta.page - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNext() {
|
||||||
|
if (props.paginationMeta.hasNext) {
|
||||||
|
handlePageChange(props.paginationMeta.page + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLast() {
|
||||||
|
if (props.paginationMeta.hasNext) {
|
||||||
|
handlePageChange(props.paginationMeta.totalPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties for formatted numbers
|
||||||
|
const formattedRecordCount = computed(() => {
|
||||||
|
const count = props.paginationMeta.recordCount
|
||||||
|
if (count == null || count === undefined) return '0'
|
||||||
|
return Number(count).toLocaleString('id-ID')
|
||||||
|
})
|
||||||
|
|
||||||
|
const startRecord = computed(() => {
|
||||||
|
return ((props.paginationMeta.page - 1) * props.paginationMeta.pageSize) + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const endRecord = computed(() => {
|
||||||
|
return Math.min(props.paginationMeta.page * props.paginationMeta.pageSize, props.paginationMeta.recordCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function to determine button width based on page number
|
||||||
|
function getButtonClass(pageNumber: number) {
|
||||||
|
const digits = pageNumber.toString().length
|
||||||
|
|
||||||
|
if (digits >= 4) { // 1000+ (1k+)
|
||||||
|
return 'h-9 px-4 min-w-12'
|
||||||
|
} else if (digits === 3) { // 100-999
|
||||||
|
return 'h-9 px-3 min-w-10'
|
||||||
|
} else { // 1-99
|
||||||
|
return 'w-9 h-9 p-0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between px-2 py-2 w-full min-w-0">
|
||||||
|
<!-- Info text -->
|
||||||
|
<div v-if="showInfo" class="text-sm text-muted-foreground shrink-0">
|
||||||
|
Menampilkan {{ startRecord }}
|
||||||
|
hingga {{ endRecord }}
|
||||||
|
dari {{ formattedRecordCount }} data
|
||||||
|
</div>
|
||||||
|
<div v-else class="shrink-0"></div>
|
||||||
|
|
||||||
|
<!-- Spacer untuk memastikan ada ruang di tengah -->
|
||||||
|
<div class="flex-1 min-w-4"></div>
|
||||||
|
|
||||||
|
<!-- Pagination controls -->
|
||||||
|
<div class="shrink-0">
|
||||||
|
<Pagination
|
||||||
|
v-slot="{ page }" :total="paginationMeta.recordCount" :sibling-count="1" :page="paginationMeta.page"
|
||||||
|
:items-per-page="paginationMeta.pageSize" show-edges
|
||||||
|
>
|
||||||
|
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||||
|
<PaginationFirst :disabled="!paginationMeta.hasPrev" @click="handleFirst" />
|
||||||
|
<PaginationPrev :disabled="!paginationMeta.hasPrev" @click="handlePrev" />
|
||||||
|
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<PaginationListItem
|
||||||
|
v-if="item.type === 'page'" :key="index" :value="item.value" as-child
|
||||||
|
@click="handlePageChange(item.value)"
|
||||||
|
>
|
||||||
|
<Button :class="getButtonClass(item.value)" :variant="item.value === page ? 'default' : 'outline'">
|
||||||
|
{{ item.value }}
|
||||||
|
</Button>
|
||||||
|
</PaginationListItem>
|
||||||
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<PaginationNext :disabled="!paginationMeta.hasNext" @click="handleNext" />
|
||||||
|
<PaginationLast :disabled="!paginationMeta.hasNext" @click="handleLast" />
|
||||||
|
</PaginationList>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectRoot } from 'radix-vue'
|
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
|
||||||
|
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
|
||||||
import {
|
import {
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectValue,
|
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from '~/components/pub/ui/select'
|
} from '~/components/pub/ui/select'
|
||||||
import type { SelectRootProps, SelectRootEmits } from 'radix-vue'
|
|
||||||
import { useForwardPropsEmits } from 'radix-vue'
|
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md',
|
||||||
position === 'popper' &&
|
position === 'popper'
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ const iconName = computed(() => props.iconName || 'i-radix-icons-caret-sort')
|
|||||||
v-bind="forwardedProps"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'border-input ring-offset-background placeholder:text-muted-foreground 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,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<SelectIcon as-child>
|
<SelectIcon as-child class="absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
<Icon :name="iconName" class="h-4 w-4 opacity-50" />
|
<Icon name="i-radix-icons-caret-sort" class="h-4 w-4 opacity-50" />
|
||||||
</SelectIcon>
|
</SelectIcon>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# useFormErrors Composable
|
||||||
|
|
||||||
|
Composable untuk menangani form validation errors seperti Laravel. Mengkonversi ZodError menjadi format yang mudah digunakan di template.
|
||||||
|
|
||||||
|
## Penggunaan
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Di component parent (entry.vue)
|
||||||
|
const { errors, setFromZodError, clearErrors } = useFormErrors()
|
||||||
|
|
||||||
|
// Validasi dengan Zod
|
||||||
|
const result = schema.safeParse(data.value)
|
||||||
|
if (!result.success) {
|
||||||
|
setFromZodError(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Di Template
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Pass errors ke form component -->
|
||||||
|
<AppDivisonEntryForm v-model="data" :errors="errors" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Di Form Component
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import type { FormErrors } from '~/composables/useFormErrors'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: any
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Setiap Field harus memiliki id yang sesuai dengan field name -->
|
||||||
|
<Field id="name" :errors="errors">
|
||||||
|
<Input v-model="data.name" />
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `setFromZodError(zodError)` - Set errors dari ZodError
|
||||||
|
- `setErrors(errors)` - Set errors manual
|
||||||
|
- `setError(field, message)` - Set error untuk field tertentu
|
||||||
|
- `clearError(field)` - Hapus error untuk field tertentu
|
||||||
|
- `clearErrors()` - Hapus semua errors
|
||||||
|
- `hasError(field)` - Cek apakah ada error untuk field
|
||||||
|
- `getError(field)` - Ambil error message untuk field
|
||||||
|
|
||||||
|
### Computed Properties
|
||||||
|
|
||||||
|
- `hasErrors` - Boolean apakah ada error
|
||||||
|
- `errorMessages` - Array semua error messages
|
||||||
|
- `firstError` - Error pertama (untuk alert general)
|
||||||
|
|
||||||
|
## Field Component
|
||||||
|
|
||||||
|
Field component akan otomatis menampilkan error jika:
|
||||||
|
1. Field memiliki `id` prop yang sesuai dengan field name
|
||||||
|
2. Field menerima `errors` prop
|
||||||
|
3. Ada error untuk field tersebut di dalam errors object
|
||||||
|
|
||||||
|
Error akan ditampilkan dengan class `.field-error-info` yang sudah di-style dengan warna merah.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import type { ZodError } from 'zod'
|
||||||
|
import type { FormErrors } from '~/types/error'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable untuk menangani form validation errors seperti Laravel
|
||||||
|
* Mengkonversi ZodError menjadi format yang mudah digunakan di template
|
||||||
|
*/
|
||||||
|
export function useFormErrors() {
|
||||||
|
const errors = ref<FormErrors>({})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set errors dari ZodError
|
||||||
|
*/
|
||||||
|
function setFromZodError(zodError: ZodError) {
|
||||||
|
const newErrors: FormErrors = {}
|
||||||
|
|
||||||
|
zodError.errors.forEach((error) => {
|
||||||
|
const field = error.path.join('.')
|
||||||
|
newErrors[field] = {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
path: error.path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
errors.value = newErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set errors manual (untuk error dari API response)
|
||||||
|
*/
|
||||||
|
function setErrors(newErrors: FormErrors) {
|
||||||
|
errors.value = newErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set error untuk field tertentu
|
||||||
|
*/
|
||||||
|
function setError(field: string, message: string, extra: Record<string, any> = {}) {
|
||||||
|
errors.value[field] = {
|
||||||
|
message,
|
||||||
|
...extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus error untuk field tertentu
|
||||||
|
*/
|
||||||
|
function clearError(field: string) {
|
||||||
|
delete errors.value[field]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus semua errors
|
||||||
|
*/
|
||||||
|
function clearErrors() {
|
||||||
|
errors.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah ada error untuk field tertentu
|
||||||
|
*/
|
||||||
|
function hasError(field: string): boolean {
|
||||||
|
return !!errors.value[field]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil error message untuk field tertentu
|
||||||
|
*/
|
||||||
|
function getError(field: string): string | null {
|
||||||
|
return errors.value[field]?.message || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah ada error apapun
|
||||||
|
*/
|
||||||
|
const hasErrors = computed(() => Object.keys(errors.value).length > 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil semua error messages sebagai array
|
||||||
|
*/
|
||||||
|
const errorMessages = computed(() =>
|
||||||
|
Object.values(errors.value).map(error => error.message),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil error pertama (untuk menampilkan alert general)
|
||||||
|
*/
|
||||||
|
const firstError = computed(() => {
|
||||||
|
const firstKey = Object.keys(errors.value)[0]
|
||||||
|
return firstKey ? errors.value[firstKey] : null
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors: readonly(errors),
|
||||||
|
setFromZodError,
|
||||||
|
setErrors,
|
||||||
|
setError,
|
||||||
|
clearError,
|
||||||
|
clearErrors,
|
||||||
|
hasError,
|
||||||
|
getError,
|
||||||
|
hasErrors,
|
||||||
|
errorMessages,
|
||||||
|
firstError,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
|
||||||
|
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
|
||||||
|
import { refDebounced, useUrlSearchParams } from '@vueuse/core'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
// Default query schema yang bisa digunakan semua list
|
||||||
|
export const defaultQuerySchema = z.object({
|
||||||
|
q: z.union([z.literal(''), z.string().min(3)]).optional().catch(''),
|
||||||
|
page: z.coerce.number().int().min(1).default(1).catch(1),
|
||||||
|
pageSize: z.coerce.number().int().min(5).max(20).default(10).catch(10),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const defaultQueryParams = {
|
||||||
|
q: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultQueryParams = z.infer<typeof defaultQuerySchema>
|
||||||
|
|
||||||
|
interface UsePaginatedListOptions<T = any> {
|
||||||
|
// Schema untuk validasi query parameters
|
||||||
|
querySchema?: z.ZodSchema<any>
|
||||||
|
defaultQuery?: Record<string, any>
|
||||||
|
// Fungsi untuk fetch data
|
||||||
|
fetchFn: (params: any) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
body: {
|
||||||
|
data: T[]
|
||||||
|
meta: {
|
||||||
|
record_totalCount: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
// Nama endpoint untuk logging error
|
||||||
|
entityName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
|
||||||
|
const {
|
||||||
|
querySchema = defaultQuerySchema,
|
||||||
|
defaultQuery = defaultQueryParams,
|
||||||
|
fetchFn,
|
||||||
|
entityName,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const data = ref<T[]>([])
|
||||||
|
const isLoading = reactive<DataTableLoader>({
|
||||||
|
isTableLoading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// URL state management
|
||||||
|
const queryParams = useUrlSearchParams('history', {
|
||||||
|
initialValue: defaultQuery,
|
||||||
|
removeFalsyValues: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = computed(() => {
|
||||||
|
const result = querySchema.safeParse(queryParams)
|
||||||
|
return result.data || defaultQuery
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pagination state - computed from URL params
|
||||||
|
const paginationMeta = reactive<PaginationMeta>({
|
||||||
|
recordCount: 0,
|
||||||
|
page: params.value.page,
|
||||||
|
pageSize: params.value.pageSize,
|
||||||
|
totalPage: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Search model with debounce
|
||||||
|
const searchInput = ref(params.value.q || '')
|
||||||
|
const debouncedSearch = refDebounced(searchInput, 500) // 500ms debounce
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
async function fetchData() {
|
||||||
|
isLoading.isTableLoading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use current params from URL state
|
||||||
|
const currentParams = params.value
|
||||||
|
|
||||||
|
const response = await fetchFn(currentParams)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const responseBody = response.body
|
||||||
|
data.value = responseBody.data || []
|
||||||
|
|
||||||
|
const pager = responseBody.meta
|
||||||
|
// Update pagination meta from response
|
||||||
|
paginationMeta.recordCount = pager.record_totalCount
|
||||||
|
paginationMeta.page = currentParams.page
|
||||||
|
paginationMeta.pageSize = currentParams.pageSize
|
||||||
|
paginationMeta.totalPage = Math.ceil(pager.record_totalCount / paginationMeta.pageSize)
|
||||||
|
paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage
|
||||||
|
paginationMeta.hasPrev = paginationMeta.page > 1
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching ${entityName} list:`, error)
|
||||||
|
data.value = []
|
||||||
|
paginationMeta.recordCount = 0
|
||||||
|
paginationMeta.totalPage = 0
|
||||||
|
paginationMeta.hasNext = false
|
||||||
|
paginationMeta.hasPrev = false
|
||||||
|
} finally {
|
||||||
|
isLoading.isTableLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pagination page change
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
// Update URL params - this will trigger watcher
|
||||||
|
queryParams.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle search from header component
|
||||||
|
function handleSearch(searchValue: string) {
|
||||||
|
// Update URL params - this will trigger watcher and refetch data
|
||||||
|
queryParams.q = searchValue
|
||||||
|
queryParams.page = 1 // Reset to first page when searching
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
|
// Watch for URL param changes and trigger refetch
|
||||||
|
watch(params, (newParams) => {
|
||||||
|
// Sync search input with URL params (for back/forward navigation)
|
||||||
|
if (newParams.q !== searchInput.value) {
|
||||||
|
searchInput.value = newParams.q || ''
|
||||||
|
}
|
||||||
|
fetchData()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// Watch debounced search and update URL params (keeping for backward compatibility)
|
||||||
|
watch(debouncedSearch, (newValue) => {
|
||||||
|
// Only search if 3+ characters or empty (to clear search)
|
||||||
|
if (newValue.length === 0 || newValue.length >= 3) {
|
||||||
|
queryParams.q = newValue
|
||||||
|
queryParams.page = 1 // Reset to first page when searching
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize data on mount
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
paginationMeta,
|
||||||
|
searchInput,
|
||||||
|
params,
|
||||||
|
queryParams,
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
fetchData,
|
||||||
|
handlePageChange,
|
||||||
|
handleSearch,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
import type { Pinia } from 'pinia'
|
import type { Pinia } from 'pinia'
|
||||||
|
import type { XError, XErrors } from '~/types/error'
|
||||||
export interface XError {
|
|
||||||
code: string
|
|
||||||
message: string
|
|
||||||
expectedVal?: string
|
|
||||||
givenVal?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type XErrors = Record<string, XError>
|
|
||||||
|
|
||||||
export interface XfetchResult {
|
export interface XfetchResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export interface ItemPrice {
|
||||||
|
id: string
|
||||||
|
item_id: number
|
||||||
|
price: number
|
||||||
|
insuranceCompany_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
item_id: number
|
||||||
|
price: number
|
||||||
|
insuranceCompany_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): CreateDto {
|
||||||
|
return {
|
||||||
|
item_id: 1,
|
||||||
|
price: 1,
|
||||||
|
insuranceCompany_code: 'test',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export interface Item {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
itemGroup_code: string
|
||||||
|
uom_code: string
|
||||||
|
infra_id: number
|
||||||
|
stock: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
itemGroup_code: string
|
||||||
|
uom_code: string
|
||||||
|
infra_id: number
|
||||||
|
stock: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): CreateDto {
|
||||||
|
return {
|
||||||
|
name: 'test',
|
||||||
|
code: 'test',
|
||||||
|
itemGroup_code: 'test',
|
||||||
|
uom_code: 'test',
|
||||||
|
infra_id: 1,
|
||||||
|
stock: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface MedicineGroup {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): CreateDto {
|
||||||
|
return {
|
||||||
|
name: 'name',
|
||||||
|
code: 'code',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface MedicineMethod {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): CreateDto {
|
||||||
|
return {
|
||||||
|
name: 'name',
|
||||||
|
code: 'code',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
export interface Medicine {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
medicineGroup_code: string
|
||||||
|
medicineMethod_code: string
|
||||||
|
uom_code: string
|
||||||
|
type: string
|
||||||
|
dose: string
|
||||||
|
infra_id: string
|
||||||
|
stock: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDto {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
medicineGroup_code: string
|
||||||
|
medicineMethod_code: string
|
||||||
|
uom_code: string
|
||||||
|
type: string
|
||||||
|
dose: string
|
||||||
|
infra_id: string
|
||||||
|
stock: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetListDto {
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
medicineGroup_code?: string
|
||||||
|
medicineMethod_code?: string
|
||||||
|
uom_code?: string
|
||||||
|
type?: string
|
||||||
|
dose?: string
|
||||||
|
infra_id?: string
|
||||||
|
stock?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDetailDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDto extends CreateDto {
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDto {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genMedicine(): CreateDto {
|
||||||
|
return {
|
||||||
|
name: 'name',
|
||||||
|
code: 'code',
|
||||||
|
medicineGroup_code: 'medicineGroup_code',
|
||||||
|
medicineMethod_code: 'medicineMethod_code',
|
||||||
|
uom_code: 'uom_code',
|
||||||
|
type: 'type',
|
||||||
|
dose: 'dose',
|
||||||
|
infra_id: 'infra_id',
|
||||||
|
stock: 'stock',
|
||||||
|
status: 'status',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ const canCreate = hasCreateAccess(roleAccess)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="canCreate">
|
<div v-if="canCreate">
|
||||||
<FlowDoctorAdd />
|
<ContentDoctorAdd />
|
||||||
</div>
|
</div>
|
||||||
<Error v-else :status-code="403" />
|
<Error v-else :status-code="403" />
|
||||||
</template>
|
</template>
|
||||||
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in New Issue
Block a user