Feat: add UI CRUD Surat Kontrol at Rehab Medik > kunjungan > Proses
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
<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"
|
||||||
|
:max="new Date().toISOString().split('T')[0]"
|
||||||
|
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,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,69 @@
|
|||||||
|
<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-speciality.vue'
|
||||||
|
import SelectDpjp from './_common/select-dpjp.vue'
|
||||||
|
|
||||||
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: any
|
||||||
|
initialValues?: any
|
||||||
|
errors?: FormErrors
|
||||||
|
}>()
|
||||||
|
|
||||||
|
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="controlDate"
|
||||||
|
label="Tanggal Rencana Kontrol"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectSpeciality
|
||||||
|
field-name="SpesialisSubSpesialis"
|
||||||
|
label="Spesialis/Sub Spesialis"
|
||||||
|
placeholder="Pilih Spesialis/Sub Spesialis"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
<SelectDpjp
|
||||||
|
field-name="dpjp"
|
||||||
|
label="DPJP"
|
||||||
|
placeholder="Pilih DPJP"
|
||||||
|
:errors="errors"
|
||||||
|
is-required
|
||||||
|
/>
|
||||||
|
</DE.Block>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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: 3},],
|
||||||
|
|
||||||
|
headers: [
|
||||||
|
[
|
||||||
|
{ label: 'Tgl Rencana Kontrol' },
|
||||||
|
{ label: 'Spesialis/Sub Spesialis' },
|
||||||
|
{ label: 'DPJP' },
|
||||||
|
{ label: 'Status SEP' },
|
||||||
|
{ label: 'Action' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keys: ['birth_date', 'number', 'person.name', 'birth_date', 'action'],
|
||||||
|
|
||||||
|
delKeyNames: [
|
||||||
|
{ key: 'code', label: 'Kode' },
|
||||||
|
{ key: 'name', label: 'Nama' },
|
||||||
|
],
|
||||||
|
|
||||||
|
parses: {
|
||||||
|
patientId: (rec: unknown): unknown => {
|
||||||
|
const patient = rec as Patient
|
||||||
|
return patient.number
|
||||||
|
},
|
||||||
|
identity_number: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (person.nationality == 'WNA') {
|
||||||
|
return person.passportNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.residentIdentityNumber || '-'
|
||||||
|
},
|
||||||
|
birth_date: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.birthDate == 'object' && person.birthDate) {
|
||||||
|
return (person.birthDate as Date).toLocaleDateString('id-ID')
|
||||||
|
} else if (typeof person.birthDate == 'string') {
|
||||||
|
return (person.birthDate as string).substring(0, 10)
|
||||||
|
}
|
||||||
|
return person.birthDate
|
||||||
|
},
|
||||||
|
patient_age: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
return calculateAge(person.birthDate)
|
||||||
|
},
|
||||||
|
gender: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
|
||||||
|
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
|
||||||
|
return person.gender_code
|
||||||
|
} else if (typeof person.gender_code === 'string' && person.gender_code) {
|
||||||
|
return genderCodes[person.gender_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
education: (rec: unknown): unknown => {
|
||||||
|
const { person } = rec as Patient
|
||||||
|
if (typeof person.education_code == 'number' && person.education_code >= 0) {
|
||||||
|
return person.education_code
|
||||||
|
} else if (typeof person.education_code === 'string' && person.education_code) {
|
||||||
|
return educationCodes[person.education_code] || '-'
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
action(rec, idx) {
|
||||||
|
return {
|
||||||
|
idx,
|
||||||
|
rec: rec as object,
|
||||||
|
component: action,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
htmls: {
|
||||||
|
patient_address(_rec) {
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||||
|
import DetailSection from '~/components/pub/my-ui/form/view/detail-section.vue'
|
||||||
|
import { formatAddress } from '~/models/person-address'
|
||||||
|
|
||||||
|
import {
|
||||||
|
addressLocationTypeCode,
|
||||||
|
educationCodes,
|
||||||
|
genderCodes,
|
||||||
|
occupationCodes,
|
||||||
|
personContactTypes,
|
||||||
|
relationshipCodes,
|
||||||
|
religionCodes,
|
||||||
|
} from '~/lib/constants'
|
||||||
|
import { cn, mapToComboboxOptList } from '~/lib/utils'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
patient: Patient
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'click', type: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const genderOptions = mapToComboboxOptList(genderCodes)
|
||||||
|
const religionOptions = mapToComboboxOptList(religionCodes)
|
||||||
|
const educationOptions = mapToComboboxOptList(educationCodes)
|
||||||
|
const occupationOptions = mapToComboboxOptList(occupationCodes)
|
||||||
|
const relationshipOptions = mapToComboboxOptList(relationshipCodes)
|
||||||
|
const personContactTypeOptions = mapToComboboxOptList(personContactTypes)
|
||||||
|
|
||||||
|
// Computed addresses from nested data
|
||||||
|
const domicileAddress = computed(() => {
|
||||||
|
const addresses = props.patient.person.addresses
|
||||||
|
const resident = addresses?.find((addr) => addr.locationType_code === 'domicile')
|
||||||
|
return formatAddress(resident)
|
||||||
|
})
|
||||||
|
|
||||||
|
const identityAddress = computed(() => {
|
||||||
|
const addresses = props.patient.person.addresses
|
||||||
|
const primary = addresses?.find((addr) => addr.locationType_code === 'identity')
|
||||||
|
return formatAddress(primary)
|
||||||
|
})
|
||||||
|
|
||||||
|
const patientAge = computed(() => {
|
||||||
|
if (!props.patient.person.birthDate) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const birthDate = new Date(props.patient.person.birthDate)
|
||||||
|
const today = new Date()
|
||||||
|
let age = today.getFullYear() - birthDate.getFullYear()
|
||||||
|
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||||
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
return age
|
||||||
|
})
|
||||||
|
// #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">{{ patient.number || '-' }}</DetailRow>
|
||||||
|
<DetailRow label="Spesialis/Sub Spesialis">{{ patient.number || '-' }}</DetailRow>
|
||||||
|
<DetailRow label="DPJP">{{ patient.number || '-' }}</DetailRow>
|
||||||
|
<DetailRow label="Status SEP">{{ patient.number || '-' }}</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>
|
||||||
@@ -15,6 +15,7 @@ import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
|||||||
import PrescriptionList from '~/components/content/prescription/list.vue'
|
import PrescriptionList from '~/components/content/prescription/list.vue'
|
||||||
import Status from '~/components/app/encounter/status.vue'
|
import Status from '~/components/app/encounter/status.vue'
|
||||||
import Consultation from '~/components/content/consultation/list.vue'
|
import Consultation from '~/components/content/consultation/list.vue'
|
||||||
|
import EncounterList from '~/components/content/outpatient/encounter/list.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -58,7 +59,7 @@ const tabs: TabItem[] = [
|
|||||||
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
||||||
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
||||||
{ value: 'resume', label: 'Resume' },
|
{ value: 'resume', label: 'Resume' },
|
||||||
{ value: 'control', label: 'Surat Kontrol' },
|
{ value: 'control', label: 'Surat Kontrol', component: EncounterList, props: { encounter: data } },
|
||||||
{ value: 'screening', label: 'Skrinning MPP' },
|
{ value: 'screening', label: 'Skrinning MPP' },
|
||||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
|
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { Patient, genPatientProps } from '~/models/patient'
|
||||||
|
import type { ExposedForm } from '~/types/form'
|
||||||
|
import type { PatientBase } from '~/models/patient'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { genPatient } from '~/models/patient'
|
||||||
|
import { PatientSchema } from '~/schemas/patient.schema'
|
||||||
|
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
|
||||||
|
import { PersonAddressSchema } from '~/schemas/person-address.schema'
|
||||||
|
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
|
||||||
|
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
|
||||||
|
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
|
||||||
|
import { uploadAttachment } from '~/services/patient.service'
|
||||||
|
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
|
||||||
|
|
||||||
|
import {
|
||||||
|
// for form entry
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/patient.handler'
|
||||||
|
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// form related state
|
||||||
|
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const isConfirmationOpen = ref(false)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmAdd() {
|
||||||
|
// handleActionClick('submit')
|
||||||
|
console.log(`tersubmit wak`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelAdd() {
|
||||||
|
isConfirmationOpen.value = false
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
isConfirmationOpen.value = true
|
||||||
|
// const patient: Patient = await composeFormData()
|
||||||
|
// let createdPatientId = 0
|
||||||
|
|
||||||
|
// const response = await handleActionSave(
|
||||||
|
// patient,
|
||||||
|
// () => {},
|
||||||
|
// () => {},
|
||||||
|
// toast,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
|
// if (!data) return
|
||||||
|
// createdPatientId = data.id
|
||||||
|
|
||||||
|
// If has callback provided redirect to callback with patientData
|
||||||
|
// if (props.callbackUrl) {
|
||||||
|
// await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Navigate to patient list or show success message
|
||||||
|
// await navigateTo('/outpatient/encounter')
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'cancel') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack()
|
||||||
|
// handleCancelForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Surat Kontrol</div>
|
||||||
|
<AppOutpatientEncounterEntryForm
|
||||||
|
ref="personPatientForm"
|
||||||
|
:schema="ControlLetterSchema"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action :enable-draft="false" @click="handleActionClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isConfirmationOpen"
|
||||||
|
title="Simpan Data"
|
||||||
|
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
|
confirm-text="Simpan"
|
||||||
|
@confirm="handleConfirmAdd"
|
||||||
|
@cancel="handleCancelAdd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { withBase } from '~/models/_base'
|
||||||
|
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||||
|
import type { Patient } from '~/models/patient'
|
||||||
|
import type { Person } from '~/models/person'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
|
||||||
|
import { getPatientDetail } from '~/services/patient.service'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
patientId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const patient = ref(
|
||||||
|
withBase<Patient>({
|
||||||
|
person: {} as Person,
|
||||||
|
personAddresses: [],
|
||||||
|
personContacts: [],
|
||||||
|
personRelatives: [],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: 'Detail Surat Kontrol',
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(async () => {
|
||||||
|
// await getPatientDetail()
|
||||||
|
const result = await getPatientDetail(props.patientId)
|
||||||
|
if (result.success) {
|
||||||
|
patient.value = result.body.data || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
function handleAction(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'edit':
|
||||||
|
// TODO: Handle edit action
|
||||||
|
console.log('outpatient-encounter-id-edit')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
goBack()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
|
||||||
|
|
||||||
|
<AppOutpatientEncounterPreview :patient="patient" @click="handleAction" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { Patient, genPatientProps } from '~/models/patient'
|
||||||
|
import type { ExposedForm } from '~/types/form'
|
||||||
|
import type { PatientBase } from '~/models/patient'
|
||||||
|
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||||
|
import { genPatient } from '~/models/patient'
|
||||||
|
import { PatientSchema } from '~/schemas/patient.schema'
|
||||||
|
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
|
||||||
|
import { PersonAddressSchema } from '~/schemas/person-address.schema'
|
||||||
|
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
|
||||||
|
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
|
||||||
|
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
|
||||||
|
import { uploadAttachment } from '~/services/patient.service'
|
||||||
|
|
||||||
|
import {
|
||||||
|
// for form entry
|
||||||
|
isReadonly,
|
||||||
|
isProcessing,
|
||||||
|
isFormEntryDialogOpen,
|
||||||
|
isRecordConfirmationOpen,
|
||||||
|
onResetState,
|
||||||
|
handleActionSave,
|
||||||
|
handleCancelForm,
|
||||||
|
} from '~/handlers/patient.handler'
|
||||||
|
|
||||||
|
import { toast } from '~/components/pub/ui/toast'
|
||||||
|
import { getPatientDetail } from '~/services/patient.service'
|
||||||
|
import { withBase } from '~/models/_base'
|
||||||
|
import type { Person } from '~/models/person'
|
||||||
|
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||||
|
|
||||||
|
// #region Props & Emits
|
||||||
|
const props = defineProps<{
|
||||||
|
callbackUrl?: string
|
||||||
|
patientId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// form related state
|
||||||
|
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State & Computed
|
||||||
|
const router = useRouter()
|
||||||
|
const isConfirmationOpen = ref(false)
|
||||||
|
const patient = ref(
|
||||||
|
withBase<Patient>({
|
||||||
|
person: {} as Person,
|
||||||
|
personAddresses: [],
|
||||||
|
personContacts: [],
|
||||||
|
personRelatives: [],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(async () => {
|
||||||
|
// await getPatientDetail()
|
||||||
|
const result = await getPatientDetail(props.patientId)
|
||||||
|
if (result.success) {
|
||||||
|
patient.value = result.body.data || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
function goBack() {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmAdd() {
|
||||||
|
// handleActionClick('submit')
|
||||||
|
console.log(`tersubmit wak`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelAdd() {
|
||||||
|
isConfirmationOpen.value = false
|
||||||
|
}
|
||||||
|
// #endregion region
|
||||||
|
|
||||||
|
// #region Utilities & event handlers
|
||||||
|
async function handleActionClick(eventType: string) {
|
||||||
|
if (eventType === 'submit') {
|
||||||
|
isConfirmationOpen.value = true
|
||||||
|
// const patient: Patient = await composeFormData()
|
||||||
|
// let createdPatientId = 0
|
||||||
|
|
||||||
|
// const response = await handleActionSave(
|
||||||
|
// patient,
|
||||||
|
// () => {},
|
||||||
|
// () => {},
|
||||||
|
// toast,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
|
// if (!data) return
|
||||||
|
// createdPatientId = data.id
|
||||||
|
|
||||||
|
// // If has callback provided redirect to callback with patientData
|
||||||
|
// if (props.callbackUrl) {
|
||||||
|
// await navigateTo(props.callbackUrl + '?patient-id=' + patient.id)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Navigate to patient list or show success message
|
||||||
|
// await navigateTo('/outpatient/encounter')
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'cancel') {
|
||||||
|
if (props.callbackUrl) {
|
||||||
|
await navigateTo(props.callbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack()
|
||||||
|
// handleCancelForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Update Surat Kontrol</div>
|
||||||
|
<AppOutpatientEncounterEntryForm
|
||||||
|
ref="personPatientForm"
|
||||||
|
:schema="PatientSchema"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="my-2 flex justify-end py-2">
|
||||||
|
<Action :enable-draft="false" @click="handleActionClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirmation
|
||||||
|
v-model:open="isConfirmationOpen"
|
||||||
|
title="Simpan Data"
|
||||||
|
message="Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
|
confirm-text="Simpan"
|
||||||
|
@confirm="handleConfirmAdd"
|
||||||
|
@cancel="handleCancelAdd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* component style */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||||
|
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||||
|
|
||||||
|
// #region Imports
|
||||||
|
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||||
|
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||||
|
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||||
|
|
||||||
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||||
|
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||||
|
|
||||||
|
import { getPatients, removePatient } from '~/services/patient.service'
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region State
|
||||||
|
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||||
|
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||||
|
entityName: 'patient',
|
||||||
|
})
|
||||||
|
|
||||||
|
const refSearchNav: RefSearchNav = {
|
||||||
|
onClick: () => {
|
||||||
|
// open filter modal
|
||||||
|
},
|
||||||
|
onInput: (val: string) => {
|
||||||
|
searchInput.value = val
|
||||||
|
},
|
||||||
|
onClear: () => {
|
||||||
|
searchInput.value = ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecordConfirmationOpen = ref(false)
|
||||||
|
const summaryLoading = ref(false)
|
||||||
|
|
||||||
|
const recId = ref<number>(0)
|
||||||
|
const recAction = ref<string>('')
|
||||||
|
const recItem = ref<any>(null)
|
||||||
|
|
||||||
|
const headerPrep: HeaderPrep = {
|
||||||
|
title: "Surat Kontrol",
|
||||||
|
icon: 'i-lucide-newspaper',
|
||||||
|
addNav: {
|
||||||
|
label: "Surat Kontrol",
|
||||||
|
onClick: () => navigateTo('/outpatient/encounter/add'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle Hooks
|
||||||
|
onMounted(() => {
|
||||||
|
getPatientSummary()
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Functions
|
||||||
|
async function getPatientSummary() {
|
||||||
|
try {
|
||||||
|
summaryLoading.value = true
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching patient summary:', error)
|
||||||
|
} finally {
|
||||||
|
summaryLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle confirmation result
|
||||||
|
async function handleConfirmDelete(record: any, action: string) {
|
||||||
|
console.log('Confirmed action:', action, 'for record:', record)
|
||||||
|
|
||||||
|
if (action === 'delete' && record?.id) {
|
||||||
|
try {
|
||||||
|
const result = await removePatient(record.id)
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Patient deleted successfully')
|
||||||
|
// Refresh the list
|
||||||
|
await fetchData()
|
||||||
|
} else {
|
||||||
|
console.error('Failed to delete patient:', result)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting patient:', error)
|
||||||
|
// Handle error - show error message to user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmation() {
|
||||||
|
// Reset record state when cancelled
|
||||||
|
recId.value = 0
|
||||||
|
recAction.value = ''
|
||||||
|
recItem.value = null
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Provide
|
||||||
|
provide('rec_id', recId)
|
||||||
|
provide('rec_action', recAction)
|
||||||
|
provide('rec_item', recItem)
|
||||||
|
provide('table_data_loader', isLoading)
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Watchers
|
||||||
|
watch([recId, recAction], () => {
|
||||||
|
switch (recAction.value) {
|
||||||
|
case ActionEvents.showDetail:
|
||||||
|
navigateTo({
|
||||||
|
name: 'outpatient-encounter-id',
|
||||||
|
params: { id: recId.value },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case ActionEvents.showEdit:
|
||||||
|
// TODO: Handle edit action
|
||||||
|
// isFormEntryDialogOpen.value = true
|
||||||
|
navigateTo({
|
||||||
|
name: 'outpatient-encounter-id-edit',
|
||||||
|
params: { id: recId.value },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case ActionEvents.showConfirmDelete:
|
||||||
|
// Trigger confirmation modal open
|
||||||
|
isRecordConfirmationOpen.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header v-model:search="searchInput" :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||||
|
|
||||||
|
<!-- Disable dulu, ayahab kalo diminta beneran -->
|
||||||
|
<!-- <div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||||
|
<template v-if="summaryLoading">
|
||||||
|
<SummaryCard
|
||||||
|
v-for="n in 4"
|
||||||
|
:key="n"
|
||||||
|
is-skeleton
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<SummaryCard
|
||||||
|
v-for="card in summaryData"
|
||||||
|
:key="card.title"
|
||||||
|
:stat="card"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<AppOutpatientEncounterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||||
|
|
||||||
|
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||||
|
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||||
|
<template #default="{ record }">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong>
|
||||||
|
{{ record?.id }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.firstName">
|
||||||
|
<strong>Nama:</strong>
|
||||||
|
{{ record.firstName }}
|
||||||
|
</p>
|
||||||
|
<p v-if="record?.code">
|
||||||
|
<strong>Kode:</strong>
|
||||||
|
{{ record.cellphone }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecordConfirmation>
|
||||||
|
</template>
|
||||||
@@ -123,7 +123,7 @@ const headerPrep: HeaderPrep = {
|
|||||||
addNav: {
|
addNav: {
|
||||||
label: 'Tambah',
|
label: 'Tambah',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
navigateTo('/bpjs/sep/add')
|
navigateTo('/integration/bpjs/sep/add')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function handleCancel() {
|
|||||||
<Dialog v-model:open="isOpen" :title="title" :size="size">
|
<Dialog v-model:open="isOpen" :title="title" :size="size">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Icon dan pesan -->
|
<!-- Icon dan pesan -->
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
|
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
type ClickType = 'cancel' | 'draft' | 'submit'
|
type ClickType = 'cancel' | 'draft' | 'submit'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
enableDraft: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'click', type: ClickType): void
|
(e: 'click', type: ClickType): void
|
||||||
}>()
|
}>()
|
||||||
@@ -16,7 +24,7 @@ function onClick(type: ClickType) {
|
|||||||
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
|
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="bg-orange-500" variant="outline" type="button" @click="onClick('draft')">
|
<Button v-show="enableDraft" class="bg-orange-500" variant="outline" type="button" @click="onClick('draft')">
|
||||||
<Icon name="i-lucide-file" class="me-2" />
|
<Icon name="i-lucide-file" class="me-2" />
|
||||||
Draft
|
Draft
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Update Surat Kontrol',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<ContentOutpatientEncounterEdit :patient-id="Number(route.params.id)" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Detail Surat Kontrol',
|
||||||
|
contentFrame: 'cf-container-md',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<ContentOutpatientEncounterDetail :patient-id="Number(route.params.id)" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Tambah Surat Kontrol',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
|
const callbackUrl = route.query['return-path'] as string | undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRead">
|
||||||
|
<ContentOutpatientEncounterAdd :callback-url="callbackUrl" />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,10 +1,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute();
|
import type { PagePermission } from '~/models/role'
|
||||||
|
import Error from '~/components/pub/my-ui/error/error.vue'
|
||||||
|
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['rbac'],
|
||||||
|
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
|
||||||
|
title: 'Surat Kontrol',
|
||||||
|
contentFrame: 'cf-full-width',
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: () => route.meta.title as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
|
||||||
|
|
||||||
|
const { checkRole, hasReadAccess } = useRBAC()
|
||||||
|
|
||||||
|
// Check if user has access to this page
|
||||||
|
const hasAccess = checkRole(roleAccess)
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// navigateTo('/403')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Define permission-based computed properties
|
||||||
|
// const canRead = hasReadAccess(roleAccess)
|
||||||
|
const canRead = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-10 text-center">
|
<div>
|
||||||
<div class="mb-5 text-base font-semibold">Hello world!!</div>
|
<div v-if="canRead">
|
||||||
<div>You are accessing "{{ route.fullPath }}"</div>
|
<ContentOutpatientEncounterList />
|
||||||
|
</div>
|
||||||
|
<Error v-else :status-code="403" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ const { checkRole, hasCreateAccess } = useRBAC()
|
|||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
throw createError({
|
// throw createError({
|
||||||
statusCode: 403,
|
// statusCode: 403,
|
||||||
statusMessage: 'Access denied',
|
// statusMessage: 'Access denied',
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canCreate = hasCreateAccess(roleAccess)
|
const canCreate = true // hasCreateAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ const { checkRole, hasReadAccess } = useRBAC()
|
|||||||
|
|
||||||
// Check if user has access to this page
|
// Check if user has access to this page
|
||||||
const hasAccess = checkRole(roleAccess)
|
const hasAccess = checkRole(roleAccess)
|
||||||
if (!hasAccess) {
|
// if (!hasAccess) {
|
||||||
navigateTo('/403')
|
// navigateTo('/403')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Define permission-based computed properties
|
// Define permission-based computed properties
|
||||||
const canRead = hasReadAccess(roleAccess)
|
const canRead = true // hasReadAccess(roleAccess)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const ControlLetterSchema = z.object({
|
||||||
|
sepStatus: z.string({
|
||||||
|
required_error: 'Mohon isi status SEP',
|
||||||
|
}),
|
||||||
|
SpesialisSubSpesialis: z.string({
|
||||||
|
required_error: 'Mohon isi status Spesialis/Sub Spesialis',
|
||||||
|
}),
|
||||||
|
dpjp: z.string({
|
||||||
|
required_error: 'Mohon isi status DPJP',
|
||||||
|
}),
|
||||||
|
controlDate: z.string({
|
||||||
|
required_error: 'Mohon lengkapi Tanggal Kontrol',
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(date) => {
|
||||||
|
// Jika kosong, return false untuk required validation
|
||||||
|
if (!date || date.trim() === '') return false
|
||||||
|
|
||||||
|
// Jika ada isi, validasi format tanggal
|
||||||
|
try {
|
||||||
|
const dateObj = new Date(date)
|
||||||
|
// Cek apakah tanggal valid dan tahun >= 1900
|
||||||
|
return !isNaN(dateObj.getTime()) && dateObj.getFullYear() >= 1900
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Mohon lengkapi Tanggal Kontrol dengan format yang valid',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.transform((dateStr) => new Date(dateStr).toISOString()),
|
||||||
|
})
|
||||||
|
|
||||||
|
type ControlLetterFormData = z.infer<typeof ControlLetterSchema>
|
||||||
|
|
||||||
|
export { ControlLetterSchema }
|
||||||
|
export type { ControlLetterFormData }
|
||||||
Reference in New Issue
Block a user