Feat Infra (#108)

* fix: adjustment some schemas

* fix(room): fixing integrate unit of room

* feat(warehouse): modify form and integration

* feat(counter): modify form and integration

* feat(screen): add list, form and integration

* feat(screen): add page for public screen

* fix: add on reset state at list

* fix: solve list of relation

* feat(chamber): integrate form to api chamber

* feat(bed): integrate form to api bed

* fix: add searching function on list service

* fix: rewrite style for dropdown and tree select

* fix: add sort params

* fix: add sort params on division + medicine

* feat(division-position): layouting form + list

* fix: add sort params for getValueList

* chore: modify side menu style

* chore: fix ui dashboard

* feat(division-position): add content list

* feat(division-position): add temporary page

* feat(division-position): modify content and entry form
This commit is contained in:
Muhammad Rifai
2025-10-10 20:36:07 +07:00
committed by GitHub
parent 4f0c1f4318
commit f94b6d273a
73 changed files with 2638 additions and 416 deletions
+56 -11
View File
@@ -5,6 +5,10 @@ import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue' import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue' import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Types // Types
import type { InfraFormData } from '~/schemas/infra.schema' import type { InfraFormData } from '~/schemas/infra.schema'
@@ -16,6 +20,7 @@ import { toTypedSchema } from '@vee-validate/zod'
interface Props { interface Props {
schema: z.ZodSchema<any> schema: z.ZodSchema<any>
parents: any[]
values: any values: any
isLoading?: boolean isLoading?: boolean
isReadonly?: boolean isReadonly?: boolean
@@ -34,27 +39,28 @@ const { defineField, errors, meta } = useForm({
initialValues: { initialValues: {
code: '', code: '',
name: '', name: '',
infraGroup_code: 'counter', infraGroup_code: infraGroupCodesKeys.bed,
parent_id: null, parent_id: null,
} as Partial<InfraFormData>, } as Partial<InfraFormData>,
}) })
const [code, codeAttrs] = defineField('code') const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name') const [name, nameAttrs] = defineField('name')
const [infraGroup_code, infraGroupAttrs] = defineField('infraGroup_code') const [infraGroup_code] = defineField('infraGroup_code')
const [parent_id, parentIdAttrs] = defineField('parent_id') const [parent_id, parentIdAttrs] = defineField('parent_id')
if (props.values) { if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.parent_id !== undefined) parent_id.value = props.values.parent_id if (props.values.parent_id !== undefined)
parent_id.value = props.values.parent_id ? Number(props.values.parent_id) : null
} }
const resetForm = () => { const resetForm = () => {
code.value = '' code.value = ''
name.value = '' name.value = ''
infraGroup_code.value = 'counter' infraGroup_code.value = infraGroupCodesKeys.bed
parent_id.value = null parent_id.value = null
} }
@@ -62,8 +68,8 @@ function onSubmitForm() {
const formData: InfraFormData = { const formData: InfraFormData = {
code: code.value || '', code: code.value || '',
name: name.value || '', name: name.value || '',
infraGroup_code: infraGroup_code.value || 'counter', infraGroup_code: infraGroup_code.value || infraGroupCodesKeys.bed,
parent_id: parent_id.value || null, parent_id: parent_id.value ? Number(parent_id.value) : null,
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
@@ -74,23 +80,62 @@ function onCancelForm() {
</script> </script>
<template> <template>
<form id="form-counter" @submit.prevent> <form
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> id="form-floor"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell> <Cell>
<Label height="compact">Kode</Label> <Label height="compact">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" /> <Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Nama</Label> <Label height="compact">Nama</Label>
<Field :errMessage="errors.name"> <Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" /> <Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Kamar</Label>
<Field :errMessage="errors.parent_id">
<Combobox
id="parent"
v-model="parent_id"
v-bind="parentIdAttrs"
:items="parents"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Kamar"
search-placeholder="Cari Kamar"
empty-message="Item tidak ditemukan"
/>
</Field> </Field>
</Cell> </Cell>
</Block> </Block>
<div class="my-2 flex justify-end gap-2 py-2"> <div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button> <Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button <Button
v-if="!isReadonly" v-if="!isReadonly"
type="button" type="button"
+2 -5
View File
@@ -12,9 +12,9 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Counter Induk' }, { label: '' }]] export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Kamar' }, { label: '' }]]
export const keys = ['code', 'name', 'parent', 'action'] export const keys = ['code', 'name', 'parent', 'action']
@@ -36,9 +36,6 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
+7 -2
View File
@@ -1,6 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type' // Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue' import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg' import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
interface Props { interface Props {
@@ -21,7 +26,7 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubMyUiDataTable
:rows="data" :rows="data"
:cols="cols" :cols="cols"
:header="header" :header="header"
+56 -11
View File
@@ -5,6 +5,10 @@ import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue' import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue' import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Types // Types
import type { InfraFormData } from '~/schemas/infra.schema' import type { InfraFormData } from '~/schemas/infra.schema'
@@ -16,6 +20,7 @@ import { toTypedSchema } from '@vee-validate/zod'
interface Props { interface Props {
schema: z.ZodSchema<any> schema: z.ZodSchema<any>
parents: any[]
values: any values: any
isLoading?: boolean isLoading?: boolean
isReadonly?: boolean isReadonly?: boolean
@@ -34,27 +39,28 @@ const { defineField, errors, meta } = useForm({
initialValues: { initialValues: {
code: '', code: '',
name: '', name: '',
infraGroup_code: 'counter', infraGroup_code: infraGroupCodesKeys.chamber,
parent_id: null, parent_id: null,
} as Partial<InfraFormData>, } as Partial<InfraFormData>,
}) })
const [code, codeAttrs] = defineField('code') const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name') const [name, nameAttrs] = defineField('name')
const [infraGroup_code, infraGroupAttrs] = defineField('infraGroup_code') const [infraGroup_code] = defineField('infraGroup_code')
const [parent_id, parentIdAttrs] = defineField('parent_id') const [parent_id, parentIdAttrs] = defineField('parent_id')
if (props.values) { if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.parent_id !== undefined) parent_id.value = props.values.parent_id if (props.values.parent_id !== undefined)
parent_id.value = props.values.parent_id ? Number(props.values.parent_id) : null
} }
const resetForm = () => { const resetForm = () => {
code.value = '' code.value = ''
name.value = '' name.value = ''
infraGroup_code.value = 'counter' infraGroup_code.value = infraGroupCodesKeys.chamber
parent_id.value = null parent_id.value = null
} }
@@ -62,8 +68,8 @@ function onSubmitForm() {
const formData: InfraFormData = { const formData: InfraFormData = {
code: code.value || '', code: code.value || '',
name: name.value || '', name: name.value || '',
infraGroup_code: infraGroup_code.value || 'counter', infraGroup_code: infraGroup_code.value || infraGroupCodesKeys.chamber,
parent_id: parent_id.value || null, parent_id: parent_id.value ? Number(parent_id.value) : null,
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
@@ -74,23 +80,62 @@ function onCancelForm() {
</script> </script>
<template> <template>
<form id="form-counter" @submit.prevent> <form
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> id="form-floor"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell> <Cell>
<Label height="compact">Kode</Label> <Label height="compact">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" /> <Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Nama</Label> <Label height="compact">Nama</Label>
<Field :errMessage="errors.name"> <Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" /> <Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Lantai</Label>
<Field :errMessage="errors.parent_id">
<Combobox
id="parent"
v-model="parent_id"
v-bind="parentIdAttrs"
:items="parents"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Lantai"
search-placeholder="Cari Lantai"
empty-message="Item tidak ditemukan"
/>
</Field> </Field>
</Cell> </Cell>
</Block> </Block>
<div class="my-2 flex justify-end gap-2 py-2"> <div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button> <Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button <Button
v-if="!isReadonly" v-if="!isReadonly"
type="button" type="button"
+2 -5
View File
@@ -12,9 +12,9 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Counter Induk' }, { label: '' }]] export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Lantai' }, { label: '' }]]
export const keys = ['code', 'name', 'parent', 'action'] export const keys = ['code', 'name', 'parent', 'action']
@@ -36,9 +36,6 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
+7 -2
View File
@@ -1,6 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type' // Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue' import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg' import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
interface Props { interface Props {
@@ -21,7 +26,7 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubMyUiDataTable
:rows="data" :rows="data"
:cols="cols" :cols="cols"
:header="header" :header="header"
+3 -3
View File
@@ -37,7 +37,7 @@ const { defineField, errors, meta } = useForm({
initialValues: { initialValues: {
code: '', code: '',
name: '', name: '',
infraGroup_code: infraGroupCodesKeys.building, infraGroup_code: infraGroupCodesKeys.counter,
parent_id: null, parent_id: null,
} as Partial<InfraFormData>, } as Partial<InfraFormData>,
}) })
@@ -57,7 +57,7 @@ if (props.values) {
const resetForm = () => { const resetForm = () => {
code.value = '' code.value = ''
name.value = '' name.value = ''
infraGroup_code.value = infraGroupCodesKeys.building infraGroup_code.value = infraGroupCodesKeys.counter
parent_id.value = null parent_id.value = null
} }
@@ -65,7 +65,7 @@ function onSubmitForm() {
const formData: InfraFormData = { const formData: InfraFormData = {
code: code.value || '', code: code.value || '',
name: name.value || '', name: name.value || '',
infraGroup_code: infraGroup_code.value || infraGroupCodesKeys.building, infraGroup_code: infraGroup_code.value || infraGroupCodesKeys.counter,
parent_id: parent_id.value || null, parent_id: parent_id.value || null,
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
@@ -0,0 +1,207 @@
<script setup lang="ts">
// Components
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
// Types
import type { DivisionPositionFormData } from '~/schemas/division-position.schema'
// Helpers
import type z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { genBase } from '~/models/_base'
import { genDivisionPosition } from '~/models/division-position'
interface Props {
schema: z.ZodSchema<any>
divisions: any[]
employees: any[]
values: any
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: DivisionPositionFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: genDivisionPosition() as Partial<DivisionPositionFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [division, divisionAttrs] = defineField('division_id')
const [employee, employeeAttrs] = defineField('employee_id')
const [headStatus, headStatusAttrs] = defineField('headStatus')
// RadioGroup uses string values; expose a string computed that maps to the boolean field
const headStatusStr = computed<string>({
get() {
if (headStatus.value === true) return 'true'
if (headStatus.value === false) return 'false'
return ''
},
set(v: string) {
if (v === 'true') headStatus.value = true
else if (v === 'false') headStatus.value = false
else headStatus.value = undefined
},
})
// Fill fields from props.values if provided
if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name
if (props.values.division_id !== undefined)
division.value = props.values.division_id ? Number(props.values.division_id) : null
if (props.values.employee_id !== undefined)
employee.value = props.values.employee_id ? Number(props.values.employee_id) : null
if (props.values.headStatus !== undefined) headStatus.value = !!props.values.headStatus
}
const resetForm = () => {
code.value = ''
name.value = ''
division.value = null
employee.value = null
headStatus.value = false
}
// Form submission handler
function onSubmitForm() {
const formData: DivisionPositionFormData = {
...genBase(),
name: name.value || '',
code: code.value || '',
division_id: division.value || null,
employee_id: employee.value || null,
headStatus: headStatus.value !== undefined ? headStatus.value : undefined,
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form
id="form-division-position"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell>
<Label height="compact">Kode</Label>
<Field :errMessage="errors.code">
<Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Nama</Label>
<Field :errMessage="errors.name">
<Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Posisi Divisi</Label>
<Field :errMessage="errors.division_id">
<Combobox
id="division"
v-model="division"
v-bind="divisionAttrs"
:items="divisions"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Divisi"
search-placeholder="Cari Divisi"
empty-message="Item tidak ditemukan"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Karyawan</Label>
<Field :errMessage="errors.employee_id">
<Combobox
id="employee"
v-model="employee"
v-bind="employeeAttrs"
:items="employees"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Karyawan"
search-placeholder="Cari Karyawan"
empty-message="Item tidak ditemukan"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Status Kepala</Label>
<Field :errMessage="errors.headStatus">
<RadioGroup
v-model="headStatusStr"
v-bind="headStatusAttrs"
class="flex gap-4"
>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="head-yes"
value="true"
/>
<Label for="head-yes">Ya</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="head-no"
value="false"
/>
<Label for="head-no">Tidak</Label>
</div>
</RadioGroup>
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
@@ -0,0 +1,64 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/my-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-ud.vue'))
export const cols: Col[] = [{}, {}, {}, {}, {}, { width: 50 }]
export const header: Th[][] = [
[
{ label: 'Kode' },
{ label: 'Nama' },
{ label: 'Divisi Induk' },
{ label: 'Karyawan' },
{ label: 'Status Kepala' },
{ label: '' },
],
]
export const keys = ['code', 'name', 'division', 'employee', 'head', 'action']
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
]
export const funcParsed: RecStrFuncUnknown = {
division: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.division?.name || '-'
},
employee: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.employee?.name || '-'
},
head: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.headStatus ? 'Ya' : 'Tidak'
},
}
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 = {}
@@ -0,0 +1,41 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { 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">
<PubMyUiDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
+2 -7
View File
@@ -37,28 +37,24 @@ const { defineField, errors, meta } = useForm({
code: '', code: '',
name: '', name: '',
parent_id: null, parent_id: null,
division_id: null,
} as Partial<DivisionFormData>, } as Partial<DivisionFormData>,
}) })
const [code, codeAttrs] = defineField('code') const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name') const [name, nameAttrs] = defineField('name')
const [parent, parentAttrs] = defineField('parent_id') const [parent, parentAttrs] = defineField('parent_id')
const [division] = defineField('division_id')
// Fill fields from props.values if provided // Fill fields from props.values if provided
if (props.values) { if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.parent_id !== undefined) parent.value = String(props.values.parent_id) if (props.values.parent_id !== undefined) parent.value = String(props.values.parent_id)
if (props.values.division_id !== undefined) division.value = String(props.values.division_id)
} }
const resetForm = () => { const resetForm = () => {
code.value = '' code.value = ''
name.value = '' name.value = ''
parent.value = null parent.value = null
division.value = null
} }
// Form submission handler // Form submission handler
@@ -68,7 +64,6 @@ function onSubmitForm() {
name: name.value || '', name: name.value || '',
code: code.value || '', code: code.value || '',
parent_id: parent.value || null, parent_id: parent.value || null,
division_id: division.value || null,
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
@@ -113,9 +108,9 @@ function onCancelForm() {
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Divisi Induk</Label> <Label height="compact">Divisi Induk</Label>
<Field :errMessage="errors.division"> <Field :errMessage="errors.parent_id">
<TreeSelect <TreeSelect
id="division" id="parent"
v-model="parent" v-model="parent"
v-bind="parentAttrs" v-bind="parentAttrs"
:data="divisions" :data="divisions"
+33 -8
View File
@@ -8,7 +8,7 @@ import Button from '~/components/pub/ui/button/Button.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue' import Combobox from '~/components/pub/my-ui/form/combobox.vue'
// Constants // Constants
import { infraGroupCodesKeys } from "~/lib/constants" import { infraGroupCodesKeys } from '~/lib/constants'
// Types // Types
import type { InfraFormData } from '~/schemas/infra.schema' import type { InfraFormData } from '~/schemas/infra.schema'
@@ -53,7 +53,8 @@ if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.parent_id !== undefined) parent_id.value = String(props.values.parent_id) if (props.values.parent_id !== undefined)
parent_id.value = props.values.parent_id ? Number(props.values.parent_id) : null
} }
const resetForm = () => { const resetForm = () => {
@@ -79,18 +80,35 @@ function onCancelForm() {
</script> </script>
<template> <template>
<form id="form-floor" @submit.prevent> <form
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> id="form-floor"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell> <Cell>
<Label height="compact">Kode</Label> <Label height="compact">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" /> <Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Nama</Label> <Label height="compact">Nama</Label>
<Field :errMessage="errors.name"> <Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" /> <Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
@@ -101,7 +119,7 @@ function onCancelForm() {
v-model="parent_id" v-model="parent_id"
v-bind="parentIdAttrs" v-bind="parentIdAttrs"
:items="parents" :items="parents"
:disabled="isLoading || isReadonly" :is-disabled="isLoading || isReadonly"
placeholder="Pilih Gedung" placeholder="Pilih Gedung"
search-placeholder="Cari Gedung" search-placeholder="Cari Gedung"
empty-message="Item tidak ditemukan" empty-message="Item tidak ditemukan"
@@ -110,7 +128,14 @@ function onCancelForm() {
</Cell> </Cell>
</Block> </Block>
<div class="my-2 flex justify-end gap-2 py-2"> <div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button> <Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button <Button
v-if="!isReadonly" v-if="!isReadonly"
type="button" type="button"
@@ -0,0 +1,108 @@
<script setup lang="ts">
// Components
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue'
// Constants
import { infraGroupCodesKeys } from "~/lib/constants"
// Types
import type { InfraFormData } from '~/schemas/infra.schema'
// Helpers
import type z from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
interface Props {
schema: z.ZodSchema<any>
values: any
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const emit = defineEmits<{
submit: [values: InfraFormData, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(props.schema),
initialValues: {
code: '',
name: '',
infraGroup_code: infraGroupCodesKeys['public-screen'],
parent_id: null,
} as Partial<InfraFormData>,
})
const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name')
const [infraGroup_code] = defineField('infraGroup_code')
const [parent_id] = defineField('parent_id')
if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.parent_id !== undefined) parent_id.value = props.values.parent_id
}
const resetForm = () => {
code.value = ''
name.value = ''
infraGroup_code.value = infraGroupCodesKeys['public-screen']
parent_id.value = null
}
function onSubmitForm() {
const formData: InfraFormData = {
code: code.value || '',
name: name.value || '',
infraGroup_code: infraGroup_code.value || infraGroupCodesKeys['public-screen'],
parent_id: parent_id.value || null,
}
emit('submit', formData, resetForm)
}
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form id="form-building" @submit.prevent>
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1">
<Cell>
<Label height="compact">Kode</Label>
<Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" />
</Field>
</Cell>
<Cell>
<Label height="compact">Nama</Label>
<Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" />
</Field>
</Cell>
</Block>
<div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
@@ -0,0 +1,37 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/my-ui/data/types'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: '' }]]
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 = {}
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { 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">
<PubMyUiDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
+51 -27
View File
@@ -65,12 +65,12 @@ if (props.values) {
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.specialist_id !== undefined) if (props.values.specialist_id !== undefined)
specialist_id.value = props.values.specialist_id ? String(props.values.specialist_id) : null specialist_id.value = props.values.specialist_id ? Number(props.values.specialist_id) : null
if (props.values.subspecialist_id !== undefined) if (props.values.subspecialist_id !== undefined)
subspecialist_id.value = props.values.subspecialist_id ? String(props.values.subspecialist_id) : null subspecialist_id.value = props.values.subspecialist_id ? Number(props.values.subspecialist_id) : null
if (props.values.unit_id !== undefined) unit_id.value = props.values.unit_id ? String(props.values.unit_id) : null if (props.values.unit_id !== undefined) unit_id.value = props.values.unit_id ? Number(props.values.unit_id) : null
if (props.values.parent_id !== undefined) if (props.values.parent_id !== undefined)
parent_id.value = props.values.parent_id ? String(props.values.parent_id) : null parent_id.value = props.values.parent_id ? Number(props.values.parent_id) : null
} }
const resetForm = () => { const resetForm = () => {
@@ -102,18 +102,50 @@ function onCancelForm() {
</script> </script>
<template> <template>
<form id="form-building" @submit.prevent> <form
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> id="form-building"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell> <Cell>
<Label height="compact">Kode</Label> <Label height="compact">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" /> <Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Nama</Label> <Label height="compact">Nama</Label>
<Field :errMessage="errors.name"> <Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" /> <Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Lantai</Label>
<Field :errMessage="errors.parent_id">
<Combobox
id="parent"
v-model="parent_id"
v-bind="parentAttrs"
:items="parents"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Lantai"
search-placeholder="Cari Lantai"
empty-message="Lantai tidak ditemukan"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
@@ -124,7 +156,7 @@ function onCancelForm() {
v-model="unit_id" v-model="unit_id"
v-bind="unitAttrs" v-bind="unitAttrs"
:items="units" :items="units"
:disabled="isLoading || isReadonly" :is-disabled="isLoading || isReadonly"
placeholder="Pilih Unit" placeholder="Pilih Unit"
search-placeholder="Cari Unit" search-placeholder="Cari Unit"
empty-message="Unit tidak ditemukan" empty-message="Unit tidak ditemukan"
@@ -140,7 +172,7 @@ function onCancelForm() {
v-model="specialist_id" v-model="specialist_id"
v-bind="specialistAttrs" v-bind="specialistAttrs"
:items="specialists" :items="specialists"
:disabled="isLoading || isReadonly" :is-disabled="isLoading || isReadonly"
placeholder="Pilih Spesialis" placeholder="Pilih Spesialis"
search-placeholder="Cari spesialis" search-placeholder="Cari spesialis"
empty-message="Spesialis tidak ditemukan" empty-message="Spesialis tidak ditemukan"
@@ -156,31 +188,23 @@ function onCancelForm() {
v-model="subspecialist_id" v-model="subspecialist_id"
v-bind="subspecialistAttrs" v-bind="subspecialistAttrs"
:items="subspecialists" :items="subspecialists"
:disabled="isLoading || isReadonly" :is-disabled="isLoading || isReadonly"
placeholder="Pilih Sub Spesialis" placeholder="Pilih Sub Spesialis"
search-placeholder="Cari sub spesialis" search-placeholder="Cari sub spesialis"
empty-message="Sub Spesialis tidak ditemukan" empty-message="Sub Spesialis tidak ditemukan"
/> />
</Field> </Field>
</Cell> </Cell>
<Cell>
<Label height="compact">Lantai</Label>
<Field :errMessage="errors.parent_id">
<Combobox
id="parent"
v-model="parent_id"
v-bind="parentAttrs"
:items="parents"
:disabled="isLoading || isReadonly"
placeholder="Pilih Lantai"
search-placeholder="Cari Lantai"
empty-message="Lantai tidak ditemukan"
/>
</Field>
</Cell>
</Block> </Block>
<div class="my-2 flex justify-end gap-2 py-2"> <div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button> <Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button <Button
v-if="!isReadonly" v-if="!isReadonly"
type="button" type="button"
+6 -11
View File
@@ -14,18 +14,9 @@ const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dr
export const cols: Col[] = [{}, {}, {}, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, {}, {}, { width: 50 }]
export const header: Th[][] = [ export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Lantai' }, { label: '' }]]
[
{ label: 'Kode' },
{ label: 'Nama' },
{ label: 'Spesialis' },
{ label: 'Sub Spesialis' },
{ label: 'Unit' },
{ label: '' },
],
]
export const keys = ['code', 'name', 'specialist', 'subspecialist', 'unit', 'action'] export const keys = ['code', 'name', 'parent', 'action']
export const delKeyNames: KeyLabel[] = [ export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' }, { key: 'code', label: 'Kode' },
@@ -45,6 +36,10 @@ export const funcParsed: RecStrFuncUnknown = {
const recX = rec as SmallDetailDto const recX = rec as SmallDetailDto
return recX.unit?.name || '-' return recX.unit?.name || '-'
}, },
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
} }
export const funcComponent: RecStrFuncComponent = { export const funcComponent: RecStrFuncComponent = {
+2 -2
View File
@@ -48,7 +48,7 @@ if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.installation_id !== undefined) if (props.values.installation_id !== undefined)
installation.value = props.values.installation_id ? String(props.values.installation_id) : null installation.value = props.values.installation_id ? Number(props.values.installation_id) : null
} }
const resetForm = () => { const resetForm = () => {
@@ -58,7 +58,7 @@ const resetForm = () => {
} }
// Form submission handler // Form submission handler
function onSubmitForm(values: any) { function onSubmitForm() {
const formData: UnitFormData = { const formData: UnitFormData = {
name: name.value || '', name: name.value || '',
code: code.value || '', code: code.value || '',
+56 -11
View File
@@ -5,6 +5,10 @@ import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue' import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue' import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Button from '~/components/pub/ui/button/Button.vue' import Button from '~/components/pub/ui/button/Button.vue'
import Combobox from '~/components/pub/my-ui/form/combobox.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Types // Types
import type { InfraFormData } from '~/schemas/infra.schema' import type { InfraFormData } from '~/schemas/infra.schema'
@@ -16,6 +20,7 @@ import { toTypedSchema } from '@vee-validate/zod'
interface Props { interface Props {
schema: z.ZodSchema<any> schema: z.ZodSchema<any>
parents: any[]
values: any values: any
isLoading?: boolean isLoading?: boolean
isReadonly?: boolean isReadonly?: boolean
@@ -34,27 +39,28 @@ const { defineField, errors, meta } = useForm({
initialValues: { initialValues: {
code: '', code: '',
name: '', name: '',
infraGroup_code: 'counter', infraGroup_code: infraGroupCodesKeys.warehouse,
parent_id: null, parent_id: null,
} as Partial<InfraFormData>, } as Partial<InfraFormData>,
}) })
const [code, codeAttrs] = defineField('code') const [code, codeAttrs] = defineField('code')
const [name, nameAttrs] = defineField('name') const [name, nameAttrs] = defineField('name')
const [infraGroup_code, infraGroupAttrs] = defineField('infraGroup_code') const [infraGroup_code] = defineField('infraGroup_code')
const [parent_id, parentIdAttrs] = defineField('parent_id') const [parent_id, parentIdAttrs] = defineField('parent_id')
if (props.values) { if (props.values) {
if (props.values.code !== undefined) code.value = props.values.code if (props.values.code !== undefined) code.value = props.values.code
if (props.values.name !== undefined) name.value = props.values.name if (props.values.name !== undefined) name.value = props.values.name
if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code if (props.values.infraGroup_code !== undefined) infraGroup_code.value = props.values.infraGroup_code
if (props.values.parent_id !== undefined) parent_id.value = props.values.parent_id if (props.values.parent_id !== undefined)
parent_id.value = props.values.parent_id ? Number(props.values.parent_id) : null
} }
const resetForm = () => { const resetForm = () => {
code.value = '' code.value = ''
name.value = '' name.value = ''
infraGroup_code.value = 'counter' infraGroup_code.value = infraGroupCodesKeys.warehouse
parent_id.value = null parent_id.value = null
} }
@@ -62,8 +68,8 @@ function onSubmitForm() {
const formData: InfraFormData = { const formData: InfraFormData = {
code: code.value || '', code: code.value || '',
name: name.value || '', name: name.value || '',
infraGroup_code: infraGroup_code.value || 'counter', infraGroup_code: infraGroup_code.value || infraGroupCodesKeys.warehouse,
parent_id: parent_id.value || null, parent_id: parent_id.value ? Number(parent_id.value) : null,
} }
emit('submit', formData, resetForm) emit('submit', formData, resetForm)
} }
@@ -74,23 +80,62 @@ function onCancelForm() {
</script> </script>
<template> <template>
<form id="form-counter" @submit.prevent> <form
<Block labelSize="thin" class="!mb-2.5 !pt-0 xl:!mb-3" :colCount="1"> id="form-floor"
@submit.prevent
>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
:colCount="1"
>
<Cell> <Cell>
<Label height="compact">Kode</Label> <Label height="compact">Kode</Label>
<Field :errMessage="errors.code"> <Field :errMessage="errors.code">
<Input id="code" v-model="code" v-bind="codeAttrs" :disabled="isLoading || isReadonly" /> <Input
id="code"
v-model="code"
v-bind="codeAttrs"
:disabled="isLoading || isReadonly"
/>
</Field> </Field>
</Cell> </Cell>
<Cell> <Cell>
<Label height="compact">Nama</Label> <Label height="compact">Nama</Label>
<Field :errMessage="errors.name"> <Field :errMessage="errors.name">
<Input id="name" v-model="name" v-bind="nameAttrs" :disabled="isLoading || isReadonly" /> <Input
id="name"
v-model="name"
v-bind="nameAttrs"
:disabled="isLoading || isReadonly"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Lantai</Label>
<Field :errMessage="errors.parent_id">
<Combobox
id="parent"
v-model="parent_id"
v-bind="parentIdAttrs"
:items="parents"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih Lantai"
search-placeholder="Cari Lantai"
empty-message="Item tidak ditemukan"
/>
</Field> </Field>
</Cell> </Cell>
</Block> </Block>
<div class="my-2 flex justify-end gap-2 py-2"> <div class="my-2 flex justify-end gap-2 py-2">
<Button type="button" variant="secondary" class="w-[120px]" @click="onCancelForm"> Kembali </Button> <Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button <Button
v-if="!isReadonly" v-if="!isReadonly"
type="button" type="button"
+2 -5
View File
@@ -12,9 +12,9 @@ type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue')) const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{ width: 100 }, {}, {}, { width: 50 }] export const cols: Col[] = [{}, {}, {}, { width: 50 }]
export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Counter Induk' }, { label: '' }]] export const header: Th[][] = [[{ label: 'Kode' }, { label: 'Nama' }, { label: 'Lantai' }, { label: '' }]]
export const keys = ['code', 'name', 'parent', 'action'] export const keys = ['code', 'name', 'parent', 'action']
@@ -36,9 +36,6 @@ export const funcComponent: RecStrFuncComponent = {
idx, idx,
rec: rec as object, rec: rec as object,
component: action, component: action,
props: {
size: 'sm',
},
} }
return res return res
}, },
+7 -2
View File
@@ -1,6 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type' // Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue' import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg' import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
interface Props { interface Props {
@@ -21,7 +26,7 @@ function handlePageChange(page: number) {
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<PubBaseDataTable <PubMyUiDataTable
:rows="data" :rows="data"
:cols="cols" :cols="cols"
:header="header" :header="header"
+196 -6
View File
@@ -1,8 +1,198 @@
<!-- Duplicated from content/counter/list.vue for bed -->
<!-- TODO: Update logic and fields for bed context -->
<template>
...existing code...
</template>
<script setup lang="ts"> <script setup lang="ts">
// ...existing code... // Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppBedList from '~/components/app/bed/list.vue'
import AppBedEntryForm from '~/components/app/bed/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { InfraSchema, type InfraFormData } from '~/schemas/infra.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/infra.handler'
// Services
import { getList, getDetail, getValueLabelList } from '~/services/infra.service'
const parents = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 2,
'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.bed,
includes: 'parent',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'bed',
})
const headerPrep: HeaderPrep = {
title: 'Kasur',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(recId.value)
title.value = 'Detail Kasur'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(recId.value)
title.value = 'Edit Kasur'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
parents.value = await getValueLabelList({ sort: 'createdAt:asc', 'infraGroup-code': infraGroupCodesKeys.chamber })
await getItemList()
})
</script> </script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppBedList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Kasur'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppBedEntryForm
:schema="InfraSchema"
:parents="parents"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: InfraFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getItemList, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getItemList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
+40 -8
View File
@@ -7,7 +7,7 @@ import AppBuildingEntryForm from '~/components/app/building/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants // Constants
import { infraGroupCodesKeys } from "~/lib/constants" import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
@@ -26,6 +26,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -49,6 +50,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.building, 'infraGroup-code': infraGroupCodesKeys.building,
@@ -62,7 +64,7 @@ const headerPrep: HeaderPrep = {
title: 'Gedung', title: 'Gedung',
icon: 'i-lucide-layout-list', icon: 'i-lucide-layout-list',
refSearchNav: { refSearchNav: {
placeholder: 'Cari gedung...', placeholder: 'Cari (min. 3 karakter)...',
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
@@ -122,10 +124,31 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
<AppBuildingList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppBuildingList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Gedung'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Gedung'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppBuildingEntryForm <AppBuildingEntryForm
:schema="InfraSchema" :schema="InfraSchema"
:values="recItem" :values="recItem"
@@ -153,9 +176,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+196 -6
View File
@@ -1,8 +1,198 @@
<!-- Duplicated from content/counter/list.vue for chamber -->
<!-- TODO: Update logic and fields for chamber context -->
<template>
...existing code...
</template>
<script setup lang="ts"> <script setup lang="ts">
// ...existing code... // Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppChamberList from '~/components/app/chamber/list.vue'
import AppChamberEntryForm from '~/components/app/chamber/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { InfraSchema, type InfraFormData } from '~/schemas/infra.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/infra.handler'
// Services
import { getList, getDetail, getValueLabelList } from '~/services/infra.service'
const parents = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 2,
'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.chamber,
includes: 'parent',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'chamber',
})
const headerPrep: HeaderPrep = {
title: 'Kamar',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(recId.value)
title.value = 'Detail Kamar'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(recId.value)
title.value = 'Edit Kamar'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
parents.value = await getValueLabelList({ sort: 'createdAt:asc', 'infraGroup-code': infraGroupCodesKeys.floor })
await getItemList()
})
</script> </script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppChamberList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Kamar'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppChamberEntryForm
:schema="InfraSchema"
:parents="parents"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: InfraFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getItemList, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getItemList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
+30 -11
View File
@@ -2,9 +2,13 @@
// Components // Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue' import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue' import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppCounterList from '~/components/app/counter/list.vue'
import AppCounterEntryForm from '~/components/app/counter/entry-form.vue' import AppCounterEntryForm from '~/components/app/counter/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue' import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers // Helpers
import { usePaginatedList } from '~/composables/usePaginatedList' import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast' import { toast } from '~/components/pub/ui/toast'
@@ -22,6 +26,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -32,6 +37,7 @@ import {
import { getList, getDetail } from '~/services/infra.service' import { getList, getDetail } from '~/services/infra.service'
const title = ref('') const title = ref('')
const { const {
data, data,
isLoading, isLoading,
@@ -39,10 +45,16 @@ const {
searchInput, searchInput,
handlePageChange, handlePageChange,
handleSearch, handleSearch,
fetchData: getCounterList, fetchData: getItemList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: async ({ page, search }) => { fetchFn: async (params: any) => {
const result = await getList({ search, page, infraGroup_code: 'counter' }) const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.counter,
})
return { success: result.success || false, body: result.body || {} } return { success: result.success || false, body: result.body || {} }
}, },
entityName: 'counter', entityName: 'counter',
@@ -52,7 +64,7 @@ const headerPrep: HeaderPrep = {
title: 'Counter', title: 'Counter',
icon: 'i-lucide-layout-list', icon: 'i-lucide-layout-list',
refSearchNav: { refSearchNav: {
placeholder: 'Cari counter...', placeholder: 'Cari (min. 3 karakter)...',
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
@@ -79,7 +91,7 @@ provide('rec_action', recAction)
provide('rec_item', recItem) provide('rec_item', recItem)
provide('table_data_loader', isLoading) provide('table_data_loader', isLoading)
const getCurrentCounterDetail = async (id: number | string) => { const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id) const result = await getDetail(id)
if (result.success) { if (result.success) {
const currentValue = result.body?.data || {} const currentValue = result.body?.data || {}
@@ -91,12 +103,12 @@ const getCurrentCounterDetail = async (id: number | string) => {
watch([recId, recAction], () => { watch([recId, recAction], () => {
switch (recAction.value) { switch (recAction.value) {
case ActionEvents.showDetail: case ActionEvents.showDetail:
getCurrentCounterDetail(recId.value) getCurrentDetail(recId.value)
title.value = 'Detail Counter' title.value = 'Detail Counter'
isReadonly.value = true isReadonly.value = true
break break
case ActionEvents.showEdit: case ActionEvents.showEdit:
getCurrentCounterDetail(recId.value) getCurrentDetail(recId.value)
title.value = 'Edit Counter' title.value = 'Edit Counter'
isReadonly.value = false isReadonly.value = false
break break
@@ -107,7 +119,7 @@ watch([recId, recAction], () => {
}) })
onMounted(async () => { onMounted(async () => {
await getCounterList() await getItemList()
}) })
</script> </script>
@@ -115,6 +127,7 @@ onMounted(async () => {
<Header <Header
v-model="searchInput" v-model="searchInput"
:prep="headerPrep" :prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch" @search="handleSearch"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
@@ -129,6 +142,12 @@ onMounted(async () => {
:title="!!recItem ? title : 'Tambah Counter'" :title="!!recItem ? title : 'Tambah Counter'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppCounterEntryForm <AppCounterEntryForm
:schema="InfraSchema" :schema="InfraSchema"
@@ -138,10 +157,10 @@ onMounted(async () => {
@submit=" @submit="
(values: InfraFormData | Record<string, any>, resetForm: () => void) => { (values: InfraFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) { if (recId > 0) {
handleActionEdit(recId, values, getCounterList, resetForm, toast) handleActionEdit(recId, values, getItemList, resetForm, toast)
return return
} }
handleActionSave(values, getCounterList, resetForm, toast) handleActionSave(values, getItemList, resetForm, toast)
} }
" "
@cancel="handleCancelForm" @cancel="handleCancelForm"
@@ -152,7 +171,7 @@ onMounted(async () => {
v-model:open="isRecordConfirmationOpen" v-model:open="isRecordConfirmationOpen"
action="delete" action="delete"
:record="recItem" :record="recItem"
@confirm="() => handleActionRemove(recId, getCounterList, toast)" @confirm="() => handleActionRemove(recId, getItemList, toast)"
@cancel="" @cancel=""
> >
<template #default="{ record }"> <template #default="{ record }">
+3 -3
View File
@@ -139,11 +139,11 @@ onMounted(() => {
.join('') .join('')
}}</AvatarFallback> }}</AvatarFallback>
</Avatar> </Avatar>
<div class="grid gap-1"> <div class="grid gap-1 min-w-0">
<p class="text-sm font-medium leading-none"> <p class="text-sm font-medium leading-none truncate">
{{ recentSales.name }} {{ recentSales.name }}
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground truncate">
{{ recentSales.email }} {{ recentSales.email }}
</p> </p>
</div> </div>
@@ -0,0 +1,201 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import AppDivisionPositionList from '~/components/app/division-position/list.vue'
import AppDivisionPositionEntryForm from '~/components/app/division-position/entry-form.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { DivisionPositionSchema, type DivisionPositionFormData } from '~/schemas/division-position.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/division-position.handler'
// Services
import { getList, getDetail } from '~/services/division-position.service'
import { getValueLabelList as getDivisionLabelList } from '~/services/division.service'
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
const divisions = ref<{ value: string | number; label: string }[]>([])
const employees = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getDivisionList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'division',
})
const headerPrep: HeaderPrep = {
title: 'Divisi',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDivisionDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDivisionDetail(recId.value)
title.value = 'Detail Divisi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDivisionDetail(recId.value)
title.value = 'Edit Divisi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
divisions.value = await getDivisionLabelList({ sort: 'createdAt:asc', 'page-size': 100 })
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100 })
await getDivisionList()
})
</script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppDivisionPositionList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Divisi'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppDivisionPositionEntryForm
:schema="DivisionPositionSchema"
:divisions="divisions"
:employees="employees"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: DivisionPositionFormData | Record<string, any>, resetForm: () => void) => {
console.log(values)
if (recId > 0) {
handleActionEdit(recId, values, getDivisionList, resetForm, toast)
return
}
handleActionSave(values, getDivisionList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getDivisionList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
+11 -1
View File
@@ -13,6 +13,7 @@ import { toast } from '~/components/pub/ui/toast'
// Types // Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types' import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { DivisionSchema, type DivisionFormData } from '~/schemas/division.schema' import { DivisionSchema, type DivisionFormData } from '~/schemas/division.schema'
import type { Division } from "~/models/division"
import type { TreeItem } from '~/models/_base' import type { TreeItem } from '~/models/_base'
// Handlers // Handlers
@@ -24,6 +25,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -48,6 +50,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
includes: 'parent,childrens', includes: 'parent,childrens',
@@ -127,7 +130,8 @@ watch(
}) })
if (result.success) { if (result.success) {
const currentData = result.body.data || [] const currentData = result.body.data || []
divisionsTrees.value = getValueTreeItems(currentData || []) const normalizedData = currentData.filter((division: Division) => !division.parent_id)
divisionsTrees.value = getValueTreeItems(normalizedData)
} }
}, },
) )
@@ -156,6 +160,12 @@ onMounted(async () => {
:title="!!recItem ? title : 'Tambah Divisi'" :title="!!recItem ? title : 'Tambah Divisi'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppDivisionEntryForm <AppDivisionEntryForm
:schema="DivisionSchema" :schema="DivisionSchema"
+26 -5
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -33,7 +34,7 @@ import {
import { getList, getDetail } from '~/services/material.service' import { getList, getDetail } from '~/services/material.service'
import { getValueLabelList as getUomList } from '~/services/uom.service' import { getValueLabelList as getUomList } from '~/services/uom.service'
const uoms = ref<{ value: string; label: string }[]>([]) const uoms = ref<{ value: string | number; label: string }[]>([])
const title = ref('') const title = ref('')
const { const {
@@ -48,6 +49,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
}) })
@@ -129,13 +131,23 @@ onMounted(async () => {
:ref-search-nav="headerPrep.refSearchNav" :ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppEquipmentList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppEquipmentList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Perlengkapan'" :title="!!recItem ? title : 'Tambah Perlengkapan'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppEquipmentEntryForm <AppEquipmentEntryForm
:schema="MaterialSchema" :schema="MaterialSchema"
@@ -166,9 +178,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+41 -9
View File
@@ -26,6 +26,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -35,7 +36,7 @@ import {
// Services // Services
import { getList, getDetail, getValueLabelList } from '~/services/infra.service' import { getList, getDetail, getValueLabelList } from '~/services/infra.service'
const parents = ref<{ value: string; label: string }[]>([]) const parents = ref<{ value: string | number; label: string }[]>([])
const title = ref('') const title = ref('')
const { const {
@@ -50,6 +51,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 2, 'page-number': params['page-number'] || 2,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.floor, 'infraGroup-code': infraGroupCodesKeys.floor,
@@ -64,7 +66,7 @@ const headerPrep: HeaderPrep = {
title: 'Lantai', title: 'Lantai',
icon: 'i-lucide-layout-list', icon: 'i-lucide-layout-list',
refSearchNav: { refSearchNav: {
placeholder: 'Cari lantai...', placeholder: 'Cari (min. 3 karakter)...',
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
@@ -119,16 +121,37 @@ watch([recId, recAction], () => {
}) })
onMounted(async () => { onMounted(async () => {
parents.value = await getValueLabelList({ 'infraGroup-code': infraGroupCodesKeys.building }) parents.value = await getValueLabelList({ sort: 'createdAt:asc', 'infraGroup-code': infraGroupCodesKeys.building })
await getItemList() await getItemList()
}) })
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
<AppFloorList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppFloorList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Gedung'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Lantai'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppFloorEntryForm <AppFloorEntryForm
:schema="InfraSchema" :schema="InfraSchema"
:parents="parents" :parents="parents"
@@ -157,9 +180,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+25 -4
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -48,6 +49,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
}) })
@@ -129,13 +131,23 @@ onMounted(async () => {
@search="handleSearch" @search="handleSearch"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppInstallationList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppInstallationList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Instalasi'" :title="!!recItem ? title : 'Tambah Instalasi'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppInstallationEntryForm <AppInstallationEntryForm
:schema="InstallationSchema" :schema="InstallationSchema"
@@ -166,9 +178,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -46,6 +47,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
}) })
@@ -137,6 +139,12 @@ onMounted(async () => {
:title="!!recItem ? title : 'Tambah Kelompok Obat'" :title="!!recItem ? title : 'Tambah Kelompok Obat'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppMedicineGroupEntryForm <AppMedicineGroupEntryForm
:schema="BaseSchema" :schema="BaseSchema"
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -46,6 +47,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
}) })
@@ -137,6 +139,12 @@ onMounted(async () => {
:title="!!recItem ? title : 'Tambah Metode Obat'" :title="!!recItem ? title : 'Tambah Metode Obat'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppMedicineMethodEntryForm <AppMedicineMethodEntryForm
:schema="BaseSchema" :schema="BaseSchema"
+34 -8
View File
@@ -26,6 +26,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -55,6 +56,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
includes: 'medicineGroup,medicineMethod,uom', includes: 'medicineGroup,medicineMethod,uom',
@@ -123,9 +125,9 @@ watch([recId, recAction], () => {
}) })
onMounted(async () => { onMounted(async () => {
medicineGroups.value = await getMedicineGroupList() medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 })
medicineMethods.value = await getMedicineMethodList() medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 })
uoms.value = await getUomList() uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 })
await getMedicineList() await getMedicineList()
}) })
</script> </script>
@@ -138,9 +140,24 @@ onMounted(async () => {
:ref-search-nav="headerPrep.refSearchNav" :ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppMedicineList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppMedicineList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Obat'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Obat'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppMedicineEntryForm <AppMedicineEntryForm
:schema="MedicineSchema" :schema="MedicineSchema"
:values="recItem" :values="recItem"
@@ -172,9 +189,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
@@ -0,0 +1,194 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppPublicScreenList from '~/components/app/public-screen/list.vue'
import AppPublicScreenEntryForm from '~/components/app/public-screen/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { InfraSchema, type InfraFormData } from '~/schemas/infra.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/infra.handler'
// Services
import { getList, getDetail } from '~/services/infra.service'
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys['public-screen'],
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'counter',
})
const headerPrep: HeaderPrep = {
title: 'Layar Publik',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(recId.value)
title.value = 'Detail Layar Publik'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(recId.value)
title.value = 'Edit Layar Publik'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
await getItemList()
})
</script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppPublicScreenList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Layar Publik'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppPublicScreenEntryForm
:schema="InfraSchema"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: InfraFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getItemList, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getItemList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
+92 -25
View File
@@ -26,6 +26,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -37,11 +38,14 @@ import { getList, getDetail, getValueLabelList } from '~/services/infra.service'
import { getValueLabelList as getSpecialistList } from '~/services/specialist.service' import { getValueLabelList as getSpecialistList } from '~/services/specialist.service'
import { getValueLabelList as getSubspecialistList } from '~/services/subspecialist.service' import { getValueLabelList as getSubspecialistList } from '~/services/subspecialist.service'
import { getValueLabelList as getUnitList } from '~/services/unit.service' import { getValueLabelList as getUnitList } from '~/services/unit.service'
import { getDetail as getItemDetail } from '~/services/item.service'
const parents = ref<{ value: string | number; label: string }[]>([]) const parents = ref<{ value: string | number; label: string }[]>([])
const specialists = ref<{ value: string; label: string }[]>([]) const specialists = ref<{ value: string | number; label: string }[]>([])
const subspecialists = ref<{ value: string; label: string }[]>([]) const specialistsFiltered = ref<{ value: string | number; label: string }[]>([])
const units = ref<{ value: string; label: string }[]>([]) const subspecialists = ref<{ value: string | number; label: string }[]>([])
const subspecialistsFiltered = ref<{ value: string | number; label: string }[]>([])
const units = ref<{ value: string | number; label: string }[]>([])
const selectedUnit = ref<string | number | null>(null) const selectedUnit = ref<string | number | null>(null)
const selectedSpecialist = ref<string | number | null>(null) const selectedSpecialist = ref<string | number | null>(null)
const title = ref('') const title = ref('')
@@ -58,10 +62,11 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 2, 'page-number': params['page-number'] || 2,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.room, 'infraGroup-code': infraGroupCodesKeys.room,
includes: 'parent,specialist,subspecialist,unit', includes: 'parent',
}) })
return { success: result.success || false, body: result.body || {} } return { success: result.success || false, body: result.body || {} }
}, },
@@ -72,7 +77,7 @@ const headerPrep: HeaderPrep = {
title: 'Ruangan', title: 'Ruangan',
icon: 'i-lucide-layout-list', icon: 'i-lucide-layout-list',
refSearchNav: { refSearchNav: {
placeholder: 'Cari Ruangan...', placeholder: 'Cari (min. 3 karakter)...',
minLength: 3, minLength: 3,
debounceMs: 500, debounceMs: 500,
showValidationFeedback: true, showValidationFeedback: true,
@@ -103,6 +108,34 @@ const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id) const result = await getDetail(id)
if (result.success) { if (result.success) {
const currentValue = result.body?.data || {} const currentValue = result.body?.data || {}
if (currentValue.item_id) {
const itemResult = await getItemDetail(currentValue.item_id)
if (itemResult.success) {
currentValue.item = itemResult.body?.data || {}
}
}
if (currentValue.rooms) {
const rooms: any = Array.isArray(currentValue.rooms) && currentValue.rooms.length > 0 ? currentValue.rooms[0] : {}
specialistsFiltered.value = rooms?.specialist
? [
{
value: rooms.specialist?.id ? Number(rooms.specialist.id) : '',
label: rooms.specialist?.name || '',
},
]
: []
subspecialistsFiltered.value = rooms?.subspecialist
? [
{
value: rooms.subspecialist?.id ? Number(rooms.subspecialist.id) : '',
label: rooms.subspecialist?.name || '',
},
]
: []
currentValue.unit_id = rooms?.unit_id || null
currentValue.specialist_id = rooms?.specialist_id || null
currentValue.subspecialist_id = rooms?.subspecialist_id || null
}
recItem.value = currentValue recItem.value = currentValue
isFormEntryDialogOpen.value = true isFormEntryDialogOpen.value = true
} }
@@ -126,42 +159,67 @@ watch([recId, recAction], () => {
} }
}) })
watch(selectedUnit, async (val) => { watch(selectedUnit, async (value: string | number | null) => {
if (val) { specialistsFiltered.value = []
specialists.value = await getSpecialistList({ 'unit-id': val, 'page-size': 100 }) if (value) {
selectedSpecialist.value = null selectedSpecialist.value = null
subspecialists.value = [] specialistsFiltered.value = specialists.value.filter((item: any) => Number(item.parent) === Number(value))
subspecialistsFiltered.value = []
} else { } else {
specialists.value = []
selectedSpecialist.value = null selectedSpecialist.value = null
subspecialists.value = [] specialistsFiltered.value = []
subspecialistsFiltered.value = []
} }
}) })
watch(selectedSpecialist, async (val) => { watch(selectedSpecialist, async (value: string | number | null) => {
if (val) { subspecialistsFiltered.value = []
subspecialists.value = await getSubspecialistList({ 'specialist-id': val, 'page-size': 100 }) if (value) {
subspecialistsFiltered.value = subspecialists.value.filter((item: any) => Number(item.parent) === Number(value))
} else { } else {
subspecialists.value = [] subspecialistsFiltered.value = []
} }
}) })
onMounted(async () => { onMounted(async () => {
parents.value = await getValueLabelList({ 'infraGroup-code': infraGroupCodesKeys.floor }) parents.value = await getValueLabelList({ sort: 'createdAt:asc', 'infraGroup-code': infraGroupCodesKeys.floor })
units.value = await getUnitList({ 'page-size': 100 }) specialists.value = await getSpecialistList({ sort: 'createdAt:asc', 'page-size': 100 })
subspecialists.value = await getSubspecialistList({ sort: 'createdAt:asc', 'page-size': 100 })
units.value = await getUnitList({ sort: 'createdAt:asc', 'page-size': 100 })
await getItemList() await getItemList()
}) })
</script> </script>
<template> <template>
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" class="mb-4 xl:mb-5" /> <Header
<AppRoomList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppRoomList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Ruangan'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Ruangan'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppRoomEntryForm <AppRoomEntryForm
:schema="InfraSchema" :schema="InfraSchema"
:specialists="specialists" :specialists="specialistsFiltered"
:subspecialists="subspecialists" :subspecialists="subspecialistsFiltered"
:units="units" :units="units"
:parents="parents" :parents="parents"
:values="recItem" :values="recItem"
@@ -191,9 +249,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+25 -4
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -48,6 +49,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
includes: 'unit', includes: 'unit',
@@ -130,13 +132,23 @@ onMounted(async () => {
@search="handleSearch" @search="handleSearch"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppSpecialistList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Spesialis'" :title="!!recItem ? title : 'Tambah Spesialis'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppSpecialistEntryForm <AppSpecialistEntryForm
:schema="SpecialistSchema" :schema="SpecialistSchema"
@@ -167,9 +179,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+25 -4
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -48,6 +49,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
includes: 'specialist', includes: 'specialist',
@@ -130,13 +132,23 @@ onMounted(async () => {
@search="handleSearch" @search="handleSearch"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppSubSpecialistList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppSubSpecialistList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Sub Spesialis'" :title="!!recItem ? title : 'Tambah Sub Spesialis'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppSubSpecialistEntryForm <AppSubSpecialistEntryForm
:schema="SubspecialistSchema" :schema="SubspecialistSchema"
@@ -167,9 +179,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+30 -5
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -46,7 +47,12 @@ const {
fetchData: getToolsList, fetchData: getToolsList,
} = usePaginatedList({ } = usePaginatedList({
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ search: params.search, 'page-number': params['page-number'] || 0 }) const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10,
})
return { success: result.success || false, body: result.body || {} } return { success: result.success || false, body: result.body || {} }
}, },
entityName: 'device', entityName: 'device',
@@ -130,13 +136,23 @@ onMounted(async () => {
:ref-search-nav="headerPrep.refSearchNav" :ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppToolsList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppToolsList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog <Dialog
v-model:open="isFormEntryDialogOpen" v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Peralatan'" :title="!!recItem ? title : 'Tambah Peralatan'"
size="lg" size="lg"
prevent-outside prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
> >
<AppToolsEntryForm <AppToolsEntryForm
:schema="DeviceSchema" :schema="DeviceSchema"
@@ -167,9 +183,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+31 -5
View File
@@ -23,6 +23,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -48,6 +49,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
includes: 'installation', includes: 'installation',
@@ -130,9 +132,24 @@ onMounted(async () => {
@search="handleSearch" @search="handleSearch"
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<AppUnitList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppUnitList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Unit'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Unit'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppUnitEntryForm <AppUnitEntryForm
:schema="UnitSchema" :schema="UnitSchema"
:installations="installations" :installations="installations"
@@ -162,9 +179,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+31 -5
View File
@@ -22,6 +22,7 @@ import {
isProcessing, isProcessing,
isFormEntryDialogOpen, isFormEntryDialogOpen,
isRecordConfirmationOpen, isRecordConfirmationOpen,
onResetState,
handleActionSave, handleActionSave,
handleActionEdit, handleActionEdit,
handleActionRemove, handleActionRemove,
@@ -45,6 +46,7 @@ const {
fetchFn: async (params: any) => { fetchFn: async (params: any) => {
const result = await getList({ const result = await getList({
search: params.search, search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 0, 'page-number': params['page-number'] || 0,
'page-size': params['page-size'] || 10, 'page-size': params['page-size'] || 10,
}) })
@@ -127,10 +129,25 @@ onMounted(async () => {
class="mb-4 xl:mb-5" class="mb-4 xl:mb-5"
/> />
<div class="rounded-md border p-4"> <div class="rounded-md border p-4">
<AppUomList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" /> <AppUomList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div> </div>
<Dialog v-model:open="isFormEntryDialogOpen" :title="!!recItem ? title : 'Tambah Uom'" size="lg" prevent-outside> <Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Uom'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppUomEntryForm <AppUomEntryForm
:schema="UomSchema" :schema="UomSchema"
:values="recItem" :values="recItem"
@@ -159,9 +176,18 @@ onMounted(async () => {
> >
<template #default="{ record }"> <template #default="{ record }">
<div class="text-sm"> <div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p> <p>
<p v-if="record?.name"><strong>Nama:</strong> {{ record.name }}</p> <strong>ID:</strong>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.code }}</p> {{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div> </div>
</template> </template>
</RecordConfirmation> </RecordConfirmation>
+196 -6
View File
@@ -1,8 +1,198 @@
<!-- Duplicated from content/counter/list.vue for warehouse -->
<!-- TODO: Update logic and fields for warehouse context -->
<template>
...existing code...
</template>
<script setup lang="ts"> <script setup lang="ts">
// ...existing code... // Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import AppWarehouseList from '~/components/app/warehouse/list.vue'
import AppWarehouseEntryForm from '~/components/app/warehouse/entry-form.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
// Constants
import { infraGroupCodesKeys } from '~/lib/constants'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { InfraSchema, type InfraFormData } from '~/schemas/infra.schema'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/infra.handler'
// Services
import { getList, getDetail, getValueLabelList } from '~/services/infra.service'
const parents = ref<{ value: string | number; label: string }[]>([])
const title = ref('')
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getItemList,
} = usePaginatedList({
fetchFn: async (params: any) => {
const result = await getList({
search: params.search,
sort: 'createdAt:asc',
'page-number': params['page-number'] || 2,
'page-size': params['page-size'] || 10,
'infraGroup-code': infraGroupCodesKeys.warehouse,
includes: 'parent',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'warehouse',
})
const headerPrep: HeaderPrep = {
title: 'Gudang',
icon: 'i-lucide-layout-list',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (val: string) => {
searchInput.value = val
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getCurrentDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getCurrentDetail(recId.value)
title.value = 'Detail Gudang'
isReadonly.value = true
break
case ActionEvents.showEdit:
getCurrentDetail(recId.value)
title.value = 'Edit Gudang'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
onMounted(async () => {
parents.value = await getValueLabelList({ sort: 'createdAt:asc', 'infraGroup-code': infraGroupCodesKeys.floor })
await getItemList()
})
</script> </script>
<template>
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
@search="handleSearch"
class="mb-4 xl:mb-5"
/>
<AppWarehouseList
:data="data"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
<Dialog
v-model:open="isFormEntryDialogOpen"
:title="!!recItem ? title : 'Tambah Gudang'"
size="lg"
prevent-outside
@update:open="
(value: any) => {
onResetState()
isFormEntryDialogOpen = value
}
"
>
<AppWarehouseEntryForm
:schema="InfraSchema"
:parents="parents"
:values="recItem"
:is-loading="isProcessing"
:is-readonly="isReadonly"
@submit="
(values: InfraFormData | Record<string, any>, resetForm: () => void) => {
if (recId > 0) {
handleActionEdit(recId, values, getItemList, resetForm, toast)
return
}
handleActionSave(values, getItemList, resetForm, toast)
}
"
@cancel="handleCancelForm"
/>
</Dialog>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getItemList, toast)"
@cancel=""
>
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.code }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
@@ -9,19 +9,7 @@ const props = defineProps<{
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')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
label: 'Detail', label: 'Detail',
@@ -38,6 +26,18 @@ const linkItems: LinkItem[] = [
icon: 'i-lucide-pencil', icon: 'i-lucide-pencil',
}, },
] ]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
</script> </script>
<template> <template>
@@ -46,22 +46,29 @@ const linkItems: LinkItem[] = [
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
size="lg" size="lg"
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 dark:data-[state=open]:bg-slate-800"
> >
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" /> <Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end"> <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
v-for="item in linkItems" v-for="item in linkItems"
:key="item.label" :key="item.label"
v-slot="{ active }" class="hover:bg-gray-100 dark:hover:bg-slate-700"
class="hover:bg-gray-100"
@click="item.onClick" @click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
> >
<Icon :name="item.icon" /> <Icon :name="item.icon ?? ''" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span> <span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
@@ -9,25 +9,7 @@ const props = defineProps<{
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')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
label: 'Detail', label: 'Detail',
@@ -58,6 +40,24 @@ const linkItems: LinkItem[] = [
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
}, },
] ]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script> </script>
<template> <template>
@@ -66,22 +66,29 @@ const linkItems: LinkItem[] = [
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
size="lg" size="lg"
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 dark:data-[state=open]:bg-slate-800"
> >
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" /> <Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end"> <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
v-for="item in linkItems" v-for="item in linkItems"
:key="item.label" :key="item.label"
v-slot="{ active }" class="hover:bg-gray-100 dark:hover:bg-slate-700"
class="hover:bg-gray-100"
@click="item.onClick" @click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
> >
<Icon :name="item.icon" /> <Icon :name="item.icon ?? ''" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span> <span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
@@ -9,25 +9,7 @@ const props = defineProps<{
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')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
label: 'Detail', label: 'Detail',
@@ -51,6 +33,24 @@ const linkItems: LinkItem[] = [
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
}, },
] ]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script> </script>
<template> <template>
@@ -59,22 +59,29 @@ const linkItems: LinkItem[] = [
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
size="lg" size="lg"
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 dark:data-[state=open]:bg-slate-800"
> >
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" /> <Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end"> <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
v-for="item in linkItems" v-for="item in linkItems"
:key="item.label" :key="item.label"
v-slot="{ active }" class="hover:bg-gray-100 dark:hover:bg-slate-700"
class="hover:bg-gray-100"
@click="item.onClick" @click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
> >
<Icon :name="item.icon" /> <Icon :name="item.icon ?? ''" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span> <span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
@@ -9,31 +9,7 @@ const props = defineProps<{
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')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function process() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
label: 'Proses', label: 'Proses',
@@ -64,6 +40,30 @@ const linkItems: LinkItem[] = [
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
}, },
] ]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function process() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script> </script>
<template> <template>
@@ -72,22 +72,29 @@ const linkItems: LinkItem[] = [
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
size="lg" size="lg"
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 dark:data-[state=open]:bg-slate-800"
> >
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" /> <Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end"> <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
v-for="item in linkItems" v-for="item in linkItems"
:key="item.label" :key="item.label"
v-slot="{ active }" class="hover:bg-gray-100 dark:hover:bg-slate-700"
class="hover:bg-gray-100"
@click="item.onClick" @click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
> >
<Icon :name="item.icon" /> <Icon :name="item.icon ?? ''" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span> <span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
@@ -14,19 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
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')!
const recItem = inject<Ref<any>>('rec_item')! const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
const linkItems: LinkItem[] = [ const linkItems: LinkItem[] = [
{ {
label: 'Edit', label: 'Edit',
@@ -43,6 +31,18 @@ const linkItems: LinkItem[] = [
icon: 'i-lucide-trash', icon: 'i-lucide-trash',
}, },
] ]
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script> </script>
<template> <template>
@@ -51,22 +51,29 @@ const linkItems: LinkItem[] = [
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
:size="size" :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 dark:data-[state=open]:bg-slate-800"
> >
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" /> <Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg bg-white" align="end"> <DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
v-for="item in linkItems" v-for="item in linkItems"
:key="item.label" :key="item.label"
v-slot="{ active }" class="hover:bg-gray-100 dark:hover:bg-slate-700"
class="hover:bg-gray-100"
@click="item.onClick" @click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
> >
<Icon :name="item.icon" /> <Icon :name="item.icon ?? ''" />
<span :class="active ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span> <span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
+1 -1
View File
@@ -32,7 +32,7 @@ const settingClass = computed(() => {
<template> <template>
<div :class="settingClass"> <div :class="settingClass">
<label> <label v-bind="$attrs">
<slot /> <slot />
</label> </label>
</div> </div>
+3 -3
View File
@@ -2,7 +2,7 @@
import { cn } from '~/lib/utils' import { cn } from '~/lib/utils'
interface Item { interface Item {
value: string value: string | number
label: string label: string
code?: string code?: string
priority?: number priority?: number
@@ -10,7 +10,7 @@ interface Item {
const props = defineProps<{ const props = defineProps<{
id: string id: string
modelValue?: string modelValue?: string | number
items: Item[] items: Item[]
placeholder?: string placeholder?: string
searchPlaceholder?: string searchPlaceholder?: string
@@ -20,7 +20,7 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: string] 'update:modelValue': [value: string | number]
}>() }>()
const open = ref(false) const open = ref(false)
@@ -20,7 +20,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template> <template>
<ComboboxItem <ComboboxItem
v-bind="forwarded" v-bind="forwarded"
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-normal outline-none hover:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>*]:text-sm [&>*]:font-normal', props.class)" :class="
cn(
'relative flex cursor-default select-none items-center rounded-sm bg-white px-2 py-1.5 text-sm font-normal text-black outline-none hover:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700 [&>*]:text-sm [&>*]:font-normal',
props.class,
)
"
> >
<slot /> <slot />
</ComboboxItem> </ComboboxItem>
@@ -20,11 +20,11 @@ function handleSelect(value: string) {
<div class="leaf-node min-w-max"> <div class="leaf-node min-w-max">
<CommandItem <CommandItem
:value="item.value" :value="item.value"
class="flex items-center justify-between p-2 w-full text-sm font-normal hover:text-primary cursor-pointer rounded-md" class="flex items-center justify-between p-2 w-full text-sm font-normal hover:text-primary cursor-pointer rounded-md bg-white dark:bg-transparent"
:class="{ 'pl-8': shouldAlign }" :class="{ 'pl-8': shouldAlign }"
@select="() => handleSelect(item.value)" @select="() => handleSelect(item.value)"
> >
<span class="text-sm font-normal text-gray-400">{{ item.label }}</span> <span class="text-sm font-normal text-gray-400 dark:text-gray-300">{{ item.label }}</span>
<Check <Check
v-if="selectedValue === item.value" v-if="selectedValue === item.value"
class="w-4 h-4 text-primary ml-2 flex-shrink-0" class="w-4 h-4 text-primary ml-2 flex-shrink-0"
@@ -52,19 +52,27 @@ watch(isOpen, async (newValue) => {
<template> <template>
<div class="tree-node min-w-max"> <div class="tree-node min-w-max">
<Collapsible v-model:open="isOpen" class="w-full"> <Collapsible
v-model:open="isOpen"
class="w-full"
>
<!-- Node Header --> <!-- Node Header -->
<div class="flex items-center justify-start w-full p-2 rounded-md hover:bg-accent gap-2"> <div
class="flex w-full items-center justify-start gap-2 rounded-md bg-white p-2 hover:bg-accent dark:bg-transparent dark:hover:bg-slate-700"
>
<!-- Chevron Toggle Button --> <!-- Chevron Toggle Button -->
<CollapsibleTrigger as-child> <CollapsibleTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
class="h-4 w-4 p-0 flex items-center justify-center" class="flex h-4 w-4 items-center justify-center p-0"
> >
<Loader2 v-if="isLoading" class="w-4 h-4 animate-spin text-muted-foreground" /> <Loader2
v-if="isLoading"
class="h-4 w-4 animate-spin text-muted-foreground"
/>
<ChevronRight <ChevronRight
v-else v-else
class="w-4 h-4 transition-transform duration-200 ease-in-out text-muted-foreground" class="h-4 w-4 text-muted-foreground transition-transform duration-200 ease-in-out"
:class="{ :class="{
'rotate-90': isChevronRotated, 'rotate-90': isChevronRotated,
}" }"
@@ -74,21 +82,24 @@ watch(isOpen, async (newValue) => {
<!-- Node Label --> <!-- Node Label -->
<span <span
class="text-sm font-normal cursor-pointer hover:text-primary flex-1 flex items-center justify-between" class="flex flex-1 cursor-pointer items-center justify-between text-sm font-normal text-black hover:text-primary dark:text-white"
@click="handleLabelClick" @click="handleLabelClick"
> >
{{ item.label }} {{ item.label }}
<!-- Check Icon untuk selected state --> <!-- Check Icon untuk selected state -->
<Check <Check
v-if="selectedValue === item.value" v-if="selectedValue === item.value"
class="w-4 h-4 text-primary ml-2 flex-shrink-0" class="ml-2 h-4 w-4 flex-shrink-0 text-primary"
/> />
</span> </span>
</div> </div>
<!-- Children Container --> <!-- Children Container -->
<CollapsibleContent class="pl-6"> <CollapsibleContent class="pl-6">
<div v-if="!hasChildren" class="text-sm text-muted-foreground p-2"> <div
v-if="!hasChildren"
class="p-2 text-sm text-muted-foreground"
>
{{ isLoading ? 'Memuat...' : 'Tidak ada data' }} {{ isLoading ? 'Memuat...' : 'Tidak ada data' }}
</div> </div>
<TreeView <TreeView
@@ -106,7 +117,7 @@ watch(isOpen, async (newValue) => {
<style scoped> <style scoped>
.tree-node { .tree-node {
@apply w-full; width: 100%;
} }
/* Animasi tambahan untuk smooth transition */ /* Animasi tambahan untuk smooth transition */
@@ -37,7 +37,7 @@ const filteredData = computed(() => {
// recursive filter // recursive filter
function filterTree(items: TreeItem[]): TreeItem[] { function filterTree(items: TreeItem[]): TreeItem[] {
return items return items
.map(item => { .map((item) => {
const match = item.label.toLowerCase().includes(searchValue.value.toLowerCase()) const match = item.label.toLowerCase().includes(searchValue.value.toLowerCase())
let children: TreeItem[] | undefined = undefined let children: TreeItem[] | undefined = undefined
if (item.children) { if (item.children) {
@@ -57,22 +57,28 @@ const filteredData = computed(() => {
<template> <template>
<Popover v-model:open="open"> <Popover v-model:open="open">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button variant="outline" role="combobox" class="w-full justify-between bg-white border-1 border-gray-400"> <Button
variant="outline"
role="combobox"
class="w-full justify-between border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
>
<span <span
class="font-normal text-muted-foreground" :class="cn( class="font-normal text-muted-foreground"
'font-normal', :class="cn('font-normal', !modelValue && 'text-muted-foreground', modelValue && 'text-black')"
!modelValue && 'text-muted-foreground',
modelValue && 'text-black',
)"
> >
{{ selectedLabel }} {{ selectedLabel }}
</span> </span>
<ChevronsUpDown class="w-4 h-4 ml-2 opacity-50 shrink-0" /> <ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="min-w-full max-w-[350px] p-0"> <PopoverContent
class="min-w-full max-w-[350px] border border-slate-200 bg-white p-0 text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
>
<Command> <Command>
<CommandInput placeholder="Cari item..." v-model="searchValue" /> <CommandInput
placeholder="Cari item..."
v-model="searchValue"
/>
<CommandEmpty>Item tidak ditemukan.</CommandEmpty> <CommandEmpty>Item tidak ditemukan.</CommandEmpty>
<CommandList class="max-h-[300px] overflow-x-auto overflow-y-auto"> <CommandList class="max-h-[300px] overflow-x-auto overflow-y-auto">
<CommandGroup> <CommandGroup>
@@ -4,14 +4,19 @@ import type { HTMLAttributes } from 'vue'
import { Primitive } from 'radix-vue' import { Primitive } from 'radix-vue'
import { cn } from '~/lib/utils' import { cn } from '~/lib/utils'
const props = withDefaults(defineProps<PrimitiveProps & { const props = withDefaults(
size?: 'sm' | 'md' defineProps<
isActive?: boolean PrimitiveProps & {
class?: HTMLAttributes['class'] size?: 'sm' | 'md'
}>(), { isActive?: boolean
as: 'a', class?: HTMLAttributes['class']
size: 'md', }
}) >(),
{
as: 'a',
size: 'md',
},
)
</script> </script>
<template> <template>
@@ -21,14 +26,16 @@ const props = withDefaults(defineProps<PrimitiveProps & {
:as-child="asChild" :as-child="asChild"
:data-size="size" :data-size="size"
:data-active="isActive" :data-active="isActive"
:class="cn( :class="
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', cn(
'aria-[current=page]:bg-sidebar-accent aria-[current=page]:font-medium aria-[current=page]:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 py-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:whitespace-normal [&>span:last-child]:break-words [&>svg]:size-4 [&>svg]:shrink-0',
size === 'sm' && 'text-xs', 'aria-[current=page]:bg-sidebar-accent aria-[current=page]:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground aria-[current=page]:font-medium',
size === 'md' && 'text-sm', size === 'sm' && 'text-xs',
'group-data-[collapsible=icon]:hidden', size === 'md' && 'text-sm',
props.class, 'group-data-[collapsible=icon]:hidden',
)" props.class,
)
"
> >
<slot /> <slot />
</Primitive> </Primitive>
+4 -4
View File
@@ -28,7 +28,7 @@ export { default as SidebarTrigger } from './SidebarTrigger.vue'
export { useSidebar } from './utils' export { useSidebar } from './utils'
export const sidebarMenuButtonVariants = cva( export const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-[current=page]:bg-sidebar-accent aria-[current=page]:font-medium aria-[current=page]:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-[current=page]:bg-sidebar-accent aria-[current=page]:font-medium aria-[current=page]:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:whitespace-normal [&>span:last-child]:break-words [&>svg]:size-4 [&>svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {
@@ -37,9 +37,9 @@ export const sidebarMenuButtonVariants = cva(
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
}, },
size: { size: {
default: 'h-8 text-sm', default: 'h-auto text-sm py-2',
sm: 'h-7 text-xs', sm: 'h-auto text-xs py-1.5',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', lg: 'h-auto text-sm group-data-[collapsible=icon]:!p-0',
}, },
}, },
defaultVariants: { defaultVariants: {
+10 -5
View File
@@ -256,10 +256,15 @@ export const infraGroupCodes: Record<string, string> = {
warehouse: 'Gudang / Depo', warehouse: 'Gudang / Depo',
room: 'Ruang', room: 'Ruang',
chamber: 'Kamar', chamber: 'Kamar',
bed: 'Ranjang' bed: 'Ranjang',
counter: 'Counter',
'public-screen': 'Public Screen',
} }
export const infraGroupCodesKeys: Record<string, string> = Object.keys(infraGroupCodes).reduce((acc, key) => { export const infraGroupCodesKeys: Record<string, string> = Object.keys(infraGroupCodes).reduce(
acc[key] = key (acc, key) => {
return acc acc[key] = key
}, {} as Record<string, string>) return acc
},
{} as Record<string, string>,
)
+3 -1
View File
@@ -1,8 +1,9 @@
import { type Base, genBase } from "./_base" import { type Base, genBase } from './_base'
export interface DivisionPosition extends Base { export interface DivisionPosition extends Base {
code: string code: string
name: string name: string
headStatus?: boolean
division_id: number division_id: number
employee_id?: number employee_id?: number
} }
@@ -12,6 +13,7 @@ export function genDivisionPosition(): DivisionPosition {
...genBase(), ...genBase(),
code: '', code: '',
name: '', name: '',
headStatus: false,
division_id: 0, division_id: 0,
employee_id: 0, employee_id: 0,
} }
@@ -0,0 +1,24 @@
<script setup lang="ts">
import Error from '~/components/pub/my-ui/error/error.vue'
definePageMeta({
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Layar Publik',
contentFrame: 'cf-container-lg',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const canRead = true
</script>
<template>
<template v-if="canRead">
<ContentPublicScreenList />
</template>
<Error v-else :status-code="403" />
</template>
@@ -0,0 +1,24 @@
<script setup lang="ts">
import Error from '~/components/pub/my-ui/error/error.vue'
definePageMeta({
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar Divisi Posisi',
contentFrame: 'cf-container-lg',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const canRead = true
</script>
<template>
<template v-if="canRead">
<ContentDivisionPositionList />
</template>
<Error v-else :status-code="403" />
</template>
+1 -1
View File
@@ -7,7 +7,7 @@ const DeviceSchema = z.object({
uom_code: z.string({ required_error: 'Kode unit harus diisi' }).min(1, 'Kode unit harus diisi'), uom_code: z.string({ required_error: 'Kode unit harus diisi' }).min(1, 'Kode unit harus diisi'),
}) })
type DeviceFormData = z.infer<typeof DeviceSchema> & Device type DeviceFormData = z.infer<typeof DeviceSchema> & Partial<Device>
export { DeviceSchema } export { DeviceSchema }
export type { DeviceFormData } export type { DeviceFormData }
+21
View File
@@ -0,0 +1,21 @@
import { z } from 'zod'
import type { DivisionPosition } from '~/models/division-position'
const DivisionPositionSchema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'),
name: z.string({ required_error: 'Nama harus diisi' }).min(1, 'Nama minimum 1 karakter'),
headStatus: z.boolean().optional().nullable(),
division_id: z.union([
z.string({ required_error: 'Divisi Induk harus diisi' }),
z.number({ required_error: 'Divisi Induk harus diisi' })
]).optional().nullable(),
employee_id: z.union([
z.string({ required_error: 'Karyawan harus diisi' }),
z.number({ required_error: 'Karyawan harus diisi' })
]).optional().nullable(),
})
type DivisionPositionFormData = z.infer<typeof DivisionPositionSchema> & Partial<DivisionPosition>
export { DivisionPositionSchema }
export type { DivisionPositionFormData }
+1 -3
View File
@@ -1,6 +1,5 @@
import { z } from 'zod' import { z } from 'zod'
import type { Division } from '~/models/division' import type { Division } from '~/models/division'
import type { DivisionPosition } from '~/models/division-position'
const DivisionSchema = z.object({ const DivisionSchema = z.object({
code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'), code: z.string({ required_error: 'Kode harus diisi' }).min(1, 'Kode minimum 1 karakter'),
@@ -9,10 +8,9 @@ const DivisionSchema = z.object({
z.string({ required_error: 'Divisi Induk harus diisi' }), z.string({ required_error: 'Divisi Induk harus diisi' }),
z.number({ required_error: 'Divisi Induk harus diisi' }) z.number({ required_error: 'Divisi Induk harus diisi' })
]).optional().nullable(), ]).optional().nullable(),
division_id: z.string({ required_error: 'Divisi Induk harus diisi' }).optional().nullable(),
}) })
type DivisionFormData = z.infer<typeof DivisionSchema> & (Division | DivisionPosition) type DivisionFormData = z.infer<typeof DivisionSchema> & Partial<Division>
export { DivisionSchema } export { DivisionSchema }
export type { DivisionFormData } export type { DivisionFormData }
+1 -1
View File
@@ -14,7 +14,7 @@ const InfraSchema = z.object({
unit_id: z.union([z.string(), z.number()]).nullable().optional(), unit_id: z.union([z.string(), z.number()]).nullable().optional(),
}) })
type InfraFormData = z.infer<typeof InfraSchema> & Infra type InfraFormData = z.infer<typeof InfraSchema> & Partial<Infra>
export { InfraSchema } export { InfraSchema }
export type { InfraFormData } export type { InfraFormData }
+2 -1
View File
@@ -1,4 +1,5 @@
import { z } from 'zod' import { z } from 'zod'
import type { Installation } from "~/models/installation"
export const InstallationSchema = z.object({ export const InstallationSchema = z.object({
code: z.string().min(1, 'Kode wajib diisi'), code: z.string().min(1, 'Kode wajib diisi'),
@@ -6,4 +7,4 @@ export const InstallationSchema = z.object({
encounterClass_code: z.string().min(1, 'Encounter Class wajib diisi').optional(), encounterClass_code: z.string().min(1, 'Encounter Class wajib diisi').optional(),
}) })
export type InstallationFormData = z.infer<typeof InstallationSchema> export type InstallationFormData = z.infer<typeof InstallationSchema> & Partial<Installation>
+1 -1
View File
@@ -8,7 +8,7 @@ const MaterialSchema = z.object({
stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')), stock: z.preprocess((val) => Number(val), z.number({ invalid_type_error: 'Stok harus berupa angka' }).min(1, 'Stok harus lebih besar dari 0')),
}) })
type MaterialFormData = z.infer<typeof MaterialSchema> & Material type MaterialFormData = z.infer<typeof MaterialSchema> & Partial<Material>
export { MaterialSchema } export { MaterialSchema }
export type { MaterialFormData } export type { MaterialFormData }
+1 -1
View File
@@ -10,7 +10,7 @@ const SpecialistSchema = z.object({
.nullable(), .nullable(),
}) })
type SpecialistFormData = z.infer<typeof SpecialistSchema> & Specialist type SpecialistFormData = z.infer<typeof SpecialistSchema> & Partial<Specialist>
export { SpecialistSchema } export { SpecialistSchema }
export type { SpecialistFormData } export type { SpecialistFormData }
+1 -1
View File
@@ -13,7 +13,7 @@ const SubspecialistSchema = z.object({
.nullable(), .nullable(),
}) })
type SubspecialistFormData = z.infer<typeof SubspecialistSchema> & Subspecialist type SubspecialistFormData = z.infer<typeof SubspecialistSchema> & Partial<Subspecialist>
export { SubspecialistSchema } export { SubspecialistSchema }
export type { SubspecialistFormData } export type { SubspecialistFormData }
+1 -1
View File
@@ -13,7 +13,7 @@ const UnitSchema = z.object({
.nullable(), .nullable(),
}) })
type UnitFormData = z.infer<typeof UnitSchema> & Unit type UnitFormData = z.infer<typeof UnitSchema> & Partial<Unit>
export { UnitSchema } export { UnitSchema }
export type { UnitFormData } export type { UnitFormData }
-1
View File
@@ -48,7 +48,6 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
*/ */
export function getValueTreeItems(divisions: any[]): TreeItem[] { export function getValueTreeItems(divisions: any[]): TreeItem[] {
return divisions return divisions
.filter((division: Division) => !division.parent_id)
.map((division: Division) => ({ .map((division: Division) => ({
value: division.id ? String(division.id) : division.code, value: division.id ? String(division.id) : division.code,
label: division.name, label: division.name,
+38
View File
@@ -0,0 +1,38 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/employee'
const name = 'employee'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: any) => ({
value: item.id ? Number(item.id) : item.code,
label: item.name,
}))
}
return data
}
+38
View File
@@ -0,0 +1,38 @@
// Base
import * as base from './_crud-base'
const path = '/api/v1/item'
const name = 'item'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string) {
return base.getDetail(path, id, name)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: any) => ({
value: item.id ? Number(item.id) : item.code,
label: item.name,
}))
}
return data
}
+1
View File
@@ -35,6 +35,7 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
data = resultData.map((item: Specialist) => ({ data = resultData.map((item: Specialist) => ({
value: item.id ? Number(item.id) : item.code, value: item.id ? Number(item.id) : item.code,
label: item.name, label: item.name,
parent: item.unit_id ? Number(item.unit_id) : null,
})) }))
} }
return data return data
+1
View File
@@ -35,6 +35,7 @@ export async function getValueLabelList(params: any = null): Promise<{ value: st
data = resultData.map((item: Subspecialist) => ({ data = resultData.map((item: Subspecialist) => ({
value: item.id ? Number(item.id) : item.code, value: item.id ? Number(item.id) : item.code,
label: item.name, label: item.name,
parent: item.specialist_id ? Number(item.specialist_id) : null,
})) }))
} }
return data return data