Squashed commit of the following:

commit bcfb4c1456
Merge: 1cbde57 975c87d
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Mon Nov 17 11:15:14 2025 +0700

    Merge pull request #147 from dikstub-rssa/feat/surat-kontrol-135

    Feat: Integration Rehab Medik - Surat Kontrol

commit 975c87d99a
Merge: f582090 1cbde57
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Nov 17 10:58:10 2025 +0700

    Merge branch 'dev' into feat/surat-kontrol-135

commit f582090d18
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Thu Nov 13 11:56:21 2025 +0700

    Fix: Refactor surat kontrol

commit a14c4a5d3c
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Tue Nov 11 14:21:58 2025 +0700

    Fix: Refactor Surat Kontrol CRUD {id} to {code}

commit 24313adef6
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Fri Nov 7 10:35:46 2025 +0700

    Fix: debug back btn in add, edit, detail content page

commit 59b44b5729
Merge: 99a61a0 db15ec9
Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
Date:   Fri Nov 7 09:11:10 2025 +0700

    Merge branch 'dev' into feat/surat-kontrol-135

commit 99a61a0bf2
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Thu Nov 6 08:06:01 2025 +0700

    Feat: add right & bottom label in input base component

commit db48919325
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:53:43 2025 +0700

    Feat: add banner in List if requirement not met

commit bd57250f7e
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:26:48 2025 +0700

    Fix: refactor getDetail url param

commit a361922e32
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:19:07 2025 +0700

    Feat: Add & integrate add, edit, detail page

commit 331f4a6b20
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Tue Nov 4 16:56:08 2025 +0700

    Feat: Integrate Control Letter

commit 2275f4dc99
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Oct 27 14:01:58 2025 +0700

    Feat: add UI BPJS > Surat Kontrol

commit 89e0e7a2c8
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Oct 27 10:21:59 2025 +0700

    Feat: add UI CRUD Surat Kontrol at Rehab Medik > kunjungan > Proses
This commit is contained in:
hasyim_kai
2025-11-18 12:58:58 +07:00
parent dc0bcc3606
commit c98018bb4e
47 changed files with 2696 additions and 31 deletions
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { ActionEvents, type LinkItem, type ListItemDto } from '~/components/pub/my-ui/data/types';
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Print',
onClick: () => {
print()
},
icon: 'i-lucide-printer',
},
{
label: 'Log History',
onClick: () => {
history()
},
icon: 'i-lucide-logs',
},
{
label: 'Hapus',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
}
function history() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
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"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<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>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
import Field from '~/components/pub/my-ui/form/field.vue'
import Label from '~/components/pub/my-ui/form/label.vue'
import Select from '~/components/pub/my-ui/form/select.vue'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
import { cn } from '~/lib/utils'
const props = defineProps()
const items = reactive([
{ id: 1, description: 'Shipped from warehouse', createdAt: new Date(Date.now() - 86400000 * 2) },
{ id: 2, description: 'In transit to distribution center', createdAt: new Date(Date.now() - 86400000) },
{ id: 3, description: 'Out for delivery (Current)', createdAt: new Date() },
])
const itemsCount = computed(() => items.length || 0)
</script>
<template>
<ul :class="cn('pb-5 flex flex-col min-h-[30rem]', '')">
<li v-for="(item, index) in items" :key="item.id" class="flex gap-3 items-start">
<div class="flex flex-col items-center">
<div class="h-5 w-5 rounded-full border-2 border-gray-300 flex items-center justify-center">
<div :class="cn('dark:bg-white border-gray-300 rounded-full p-1.5',
index === 0 ? 'bg-green-500' : 'bg-transparent'
)">
</div>
</div>
<hr v-if="index !== itemsCount - 1" class="h-8 w-0.5 bg-gray-300 dark:bg-gray-300" aria-hidden="true">
</div>
<div class="flex justify-between items-center min-w-96">
<div class="max-w-80">
<time :class="cn('font-medium text-gray-800 dark:text-gray-100', '')">
{{ item?.createdAt.toLocaleDateString('id-ID') }}
</time>
<h1 :class="cn('text-gray-500 dark:text-gray-400', '')">{{ item.description }}</h1>
</div>
</div>
</li>
</ul>
</template>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
const value = ref({
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
}) as Ref<DateRange>
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('w-full bg-white border-gray-400 justify-start text-left font-normal', !value && 'text-muted-foreground')"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start">
<template v-if="value.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} -
{{ df.format(value.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Pick a date </template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar
v-model="value"
initial-focus
:number-of-months="2"
@update:start-value="(startDate) => (value.start = startDate)"
/>
</PopoverContent>
</Popover>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,128 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
import Field from '~/components/pub/my-ui/form/field.vue'
import Label from '~/components/pub/my-ui/form/label.vue'
import Select from '~/components/pub/my-ui/form/select.vue'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
import { cn } from '~/lib/utils'
import SelectDateRange from './_common/select-date-range.vue'
interface InstallationFormData {
name: string
code: string
encounterClassCode: string
}
const props = defineProps<{
installation: {
msg: {
placeholder: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<InstallationFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
reset: [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: InstallationFormData = {
name: values.name || '',
code: values.code || '',
encounterClassCode: values.encounterClassCode || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onResetForm({ resetForm }: { resetForm: () => void }) {
emit('reset', resetForm)
}
const items = ref([
{ label: 'Rujukan Internal', value: 'ri' },
{ label: 'SEP Rujukan', value: 'sr' },
])
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }"
as=""
keep-values
:validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-7 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<SelectDateRange
field-name="releaseDate"
label="Tanggal Penerbitan"
placeholder="Tanggal Penerbitan"
:errors="errors"
is-required
/>
<SelectDateRange
field-name="controlPlanDate"
label="Tanggal Rencana Kontrol"
placeholder="Tanggal Rencana Kontrol"
:errors="errors"
is-required
/>
<InputBase
field-name="patientName"
label="Nama Pasien"
placeholder="Nama Pasien"
/>
<InputBase
field-name="cardNumber"
label="Nomor Kartu"
placeholder="Nomor Kartu"
/>
<InputBase
field-name="sepNumber"
label="Nomor SEP"
placeholder="Nomor SEP"
/>
<SelectOriginPolyclinic
field-name="originPolyclinic"
label="Poliklinik Asal"
placeholder="Pilih Poliklinik Asal"
:errors="errors"
is-required
/>
<SelectDestinationPolyclinic
field-name="destinationPolyclinic"
label="Poliklinik Tujuan"
placeholder="Pilih Poliklinik Tujuan"
:errors="errors"
is-required
/>
</div>
</div>
<div class="my-2 flex items-center gap-3 justify-end">
<Button @click="onResetForm" variant="secondary">Reset</Button>
<Button @click="onSubmitForm">Terapkan</Button>
</div>
</form>
</Form>
</template>
@@ -0,0 +1,108 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
import { educationCodes, genderCodes } from '~/lib/constants'
import { calculateAge } from '~/lib/utils'
const action = defineAsyncComponent(() => import('./_common/dropdown-action.vue'))
const statusBadge = defineAsyncComponent(() => import('~/components/pub/my-ui/badge/status-badge.vue'))
export const config: Config = {
cols: [{}, {}, {}, {},{}, {}, {}, {}, {}, {width: 90},{width: 10},],
headers: [
[
{ label: 'No Surat' },
{ label: 'No MR' },
{ label: 'Nama' },
{ label: 'Tgl Rencana Kontrol' },
{ label: 'Tgl Penerbitan' },
{ label: 'Klinik Asal' },
{ label: 'Klinik Tujuan' },
{ label: 'DPJP' },
{ label: 'No SEP Asal' },
{ label: 'Status' },
{ label: 'Action' },
],
],
keys: ['birth_date', 'number', 'person.name', 'birth_date', 'birth_date',
'birth_date', 'number', 'person.name', 'birth_date', 'status', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
patientId: (rec: unknown): unknown => {
const patient = rec as Patient
return patient.number
},
identity_number: (rec: unknown): unknown => {
const { person } = rec as Patient
if (person.nationality == 'WNA') {
return person.passportNumber
}
return person.residentIdentityNumber || '-'
},
birth_date: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.birthDate == 'object' && person.birthDate) {
return (person.birthDate as Date).toLocaleDateString('id-ID')
} else if (typeof person.birthDate == 'string') {
return (person.birthDate as string).substring(0, 10)
}
return person.birthDate
},
patient_age: (rec: unknown): unknown => {
const { person } = rec as Patient
return calculateAge(person.birthDate)
},
gender: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
return person.gender_code
} else if (typeof person.gender_code === 'string' && person.gender_code) {
return genderCodes[person.gender_code] || '-'
}
return '-'
},
education: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.education_code == 'number' && person.education_code >= 0) {
return person.education_code
} else if (typeof person.education_code === 'string' && person.education_code) {
return educationCodes[person.education_code] || '-'
}
return '-'
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,116 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
import { Input } from '~/components/pub/ui/input'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'birthDate',
label = 'Tanggal Lahir',
placeholder = 'Pilih tanggal lahir',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Reactive variables for age calculation
const patientAge = ref<string>('Masukkan tanggal lahir')
// Function to calculate age with years, months, and days
function calculateAge(birthDate: string | Date | undefined): string {
if (!birthDate) {
return 'Masukkan tanggal lahir'
}
try {
let dateObj: Date
if (typeof birthDate === 'string') {
dateObj = parseISO(birthDate)
} else {
dateObj = birthDate
}
const today = new Date()
// Calculate years, months, and days
const totalYears = differenceInYears(today, dateObj)
// Calculate remaining months after years
const yearsPassed = new Date(dateObj)
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
const remainingMonths = differenceInMonths(today, yearsPassed)
// Calculate remaining days after years and months
const monthsPassed = new Date(yearsPassed)
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
const remainingDays = differenceInDays(today, monthsPassed)
// Format the result
const parts = []
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
return parts.length > 0 ? parts.join(' ') : '0 Hari'
} catch {
return 'Masukkan tanggal lahir'
}
}
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Input
id="birthDate"
type="date"
min="1900-01-01"
v-bind="componentField"
:placeholder="placeholder"
@update:model-value="
(value: string | number) => {
const dateStr = typeof value === 'number' ? String(value) : value
patientAge = calculateAge(dateStr)
}
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import { getValueLabelList as getDoctorLabelList } from '~/services/doctor.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { Item } from '~/components/pub/my-ui/combobox'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const doctors = ref<Array<Item>>([])
async function fetchDpjp(specialistId: string, subspecialistId: string) {
doctors.value = await getDoctorLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
includes: 'employee-person',
// "unit-id": parseInt(unitId),
"specialist-code": String(specialistId),
"subspecialist-code": String(subspecialistId),
}, true)
}
// const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
const selectedSubSpecialistId = inject<Ref<string | null>>("selectedSubSpecialistId")!
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
watch([ selectedSpecialistId, selectedSubSpecialistId], () => {
if (selectedSpecialistId.value && selectedSubSpecialistId.value) {
console.log(`Select Doctor`)
fetchDpjp( selectedSpecialistId.value, selectedSubSpecialistId.value)
}
})
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="doctors"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
:is-disabled="selectedSubSpecialistId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import type { Item } from '~/components/pub/my-ui/combobox'
import { getValueLabelList as getSpecialistLabelList } from '~/services/specialist.service'
import { getValueLabelList as getSubspecialistLabelList } from '~/services/subspecialist.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const specialists = ref<Array<Item>>([])
async function fetchSpecialists(unitId: string) {
specialists.value = await getSpecialistLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
"unit-id": String(unitId),
}, true)
}
const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
function handleSpecialistChange(selected: string) {
selectedSpecialistId.value = selected ?? null
}
watch([selectedUnitId], () => {
if (selectedUnitId.value) {
fetchSpecialists(selectedUnitId.value)
}
})
</script>
<template>
<DE.Block :class="cn('select-field-group', fieldGroupClass, containerClass)">
<div>
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
Spesialis
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="specialists"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleSpecialistChange"
:is-disabled="selectedUnitId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</div>
</DE.Block>
</template>
@@ -0,0 +1,97 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import type { Item } from '~/components/pub/my-ui/combobox'
import { getValueLabelList as getSubspecialistLabelList } from '~/services/subspecialist.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const subspecialists = ref<Array<Item>>([])
async function fetchSubSpecialists(specialistId: string) {
subspecialists.value = await getSubspecialistLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
"specialist-code": String(specialistId),
}, true)
}
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
const selectedSubSpecialistId = inject<Ref<string | null>>("selectedSubSpecialistId")!
function handleSubSpecialistChange(selected: string) {
selectedSubSpecialistId.value = selected ?? null
}
watch([selectedSpecialistId], () => {
if (selectedSpecialistId.value) {
fetchSubSpecialists(selectedSpecialistId.value)
}
})
</script>
<template>
<DE.Block :class="cn('select-field-group', fieldGroupClass, containerClass)">
<div>
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
Sub Spesialis
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="subspecialists"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleSubSpecialistChange"
:is-disabled="selectedSpecialistId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</div>
</DE.Block>
</template>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import { getValueLabelList as getUnitLabelList } from '~/services/unit.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { Item } from '~/components/pub/my-ui/combobox'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const units = ref<Array<Item>>([])
async function fetchData() {
units.value = await getUnitLabelList({}, true)
}
const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
function handleDataChange(selected: string) {
selectedUnitId.value = selected ?? null
}
onMounted(() => {
fetchData()
})
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="units"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleDataChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import SelectDate from './_common/select-date.vue'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectSpeciality from './_common/select-specialist.vue'
import SelectDpjp from './_common/select-dpjp.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import SelectUnit from './_common/select-unit.vue'
import SelectSubspecialist from './_common/select-subspecialist.vue'
import SelectSpecialist from './_common/select-specialist.vue'
const props = defineProps<{
schema: any
initialValues?: any
errors?: FormErrors
selectedUnitId?: number | null
selectedSpecialistId?: number | null
selectedSubSpecialistId?: number | null
}>()
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
defineExpose({
validate: () => formRef.value?.validate(),
resetForm: () => formRef.value?.resetForm(),
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
values: computed(() => formRef.value?.values),
})
</script>
<template>
<Form
ref="formRef"
v-slot="{ values }"
as=""
keep-values
:validation-schema="formSchema"
:validate-on-mount="false"
validation-mode="onSubmit"
:initial-values="initialValues ? initialValues : {}"
>
<DE.Block :col-count="2" :cell-flex="false">
<InputBase
field-name="sepStatus"
label="Status Sep"
placeholder="Status Sep"
:is-disabled="true"
/>
<SelectDate
field-name="date"
label="Tanggal Rencana Kontrol"
:errors="errors"
is-required
/>
<DE.Cell :col-span="2">
<DE.Block :col-count="4" :cell-flex="false">
<SelectUnit
field-name="unit_code"
label="Unit"
placeholder="Pilih Unit"
:errors="errors"
is-required
/>
<SelectSpecialist
field-name="specialist_code"
label="Spesialis/Sub Spesialis"
placeholder="Pilih Spesialis/Sub Spesialis"
:errors="errors"
is-required
/>
<SelectSubspecialist
field-name="subspecialist_code"
label="Spesialis/Sub Spesialis"
placeholder="Pilih Spesialis/Sub Spesialis"
:errors="errors"
is-required
/>
<SelectDpjp
field-name="doctor_code"
label="DPJP"
placeholder="Pilih DPJP"
:errors="errors"
is-required
/>
</DE.Block>
</DE.Cell>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,64 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
import { educationCodes, genderCodes } from '~/lib/constants'
import { calculateAge } from '~/lib/utils'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{width: 180}, {}, {}, {}, {}, {width: 30},],
headers: [
[
{ label: 'Tgl Rencana Kontrol' },
{ label: 'Spesialis' },
{ label: 'Sub Spesialis' },
{ label: 'DPJP' },
{ label: 'Status SEP' },
{ label: 'Action' },
],
],
keys: ['date', 'specialist.name', 'subspecialist.name', 'doctor.employee.person.name', 'sep_status', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
date: (rec: unknown): unknown => {
const date = (rec as any).date
if (typeof date == 'object' && date) {
return (date as Date).toLocaleDateString('id-ID')
} else if (typeof date == 'string') {
return (date as string).substring(0, 10)
}
return date
},
specialist_subspecialist: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
// const { person } = rec as Patient
return '-'
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
sep_status(_rec) {
return 'SEP Internal'
},
},
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import { cn, } from '~/lib/utils'
import type { ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
instance: ControlLetter | null
}>()
const emit = defineEmits<{
(e: 'click', type: string): void
}>()
// #endregion
// #region State & Computed
// #endregion
// Computed addresses from nested data
// #endregion
// #region Lifecycle Hooks
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onClick(type: string) {
emit('click', type)
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<div :class="cn('min-h-[50vh] space-y-2',)">
<DetailRow label="Tgl Rencana Kontrol">{{ props.instance?.date ? new Date(props.instance?.date).toLocaleDateString('id-ID') : '-' }}</DetailRow>
<DetailRow label="Unit">{{ props.instance?.unit.name || '-' }}</DetailRow>
<DetailRow label="Spesialis">{{ props.instance?.specialist.name || '-' }}</DetailRow>
<DetailRow label="Sub Spesialis">{{ props.instance?.subspecialist.name || '-' }}</DetailRow>
<DetailRow label="DPJP">{{ props.instance?.doctor.employee.person.name || '-' }}</DetailRow>
<DetailRow label="Status SEP">{{ 'SEP INTERNAL' }}</DetailRow>
</div>
<div class="border-t-1 my-2 flex justify-end border-t-slate-300 py-2">
<PubMyUiNavFooterBaEd @click="onClick" />
</div>
</template>
<style scoped></style>