Merge branch 'dev' into feat/role-check
This commit is contained in:
No files matched your search
@@ -3,3 +3,14 @@ NUXT_BPJS_API_ORIGIN=
|
|||||||
NUXT_API_VCLAIM_SWAGGER= # https://vclaim-api.multy.chat
|
NUXT_API_VCLAIM_SWAGGER= # https://vclaim-api.multy.chat
|
||||||
NUXT_SYNC_API_ORIGIN=
|
NUXT_SYNC_API_ORIGIN=
|
||||||
NUXT_API_ORIGIN=
|
NUXT_API_ORIGIN=
|
||||||
|
|
||||||
|
SSO_CONFIRM_URL =
|
||||||
|
|
||||||
|
X_AP_CODE=rssa-sso
|
||||||
|
X_AP_SECRET_KEY=sapiperah
|
||||||
|
KEYCLOAK_LOGOUT_REDIRECT=http://localhost:3000/
|
||||||
|
|
||||||
|
# test local
|
||||||
|
KEYCLOAK_REALM=rssa_testing
|
||||||
|
KEYCLOAK_URL=http://127.0.0.1:8080/
|
||||||
|
CLIENT_ID=portal-simrs-new
|
||||||
@@ -3,6 +3,7 @@ import type { z } from 'zod'
|
|||||||
import { toTypedSchema } from '@vee-validate/zod'
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
import { Loader2 } from 'lucide-vue-next'
|
import { Loader2 } from 'lucide-vue-next'
|
||||||
import { useForm } from 'vee-validate'
|
import { useForm } from 'vee-validate'
|
||||||
|
import { useKeycloak } from "~/composables/useKeycloack"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schema: z.ZodSchema<any>
|
schema: z.ZodSchema<any>
|
||||||
@@ -13,6 +14,7 @@ const props = defineProps<Props>()
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [data: any]
|
submit: [data: any]
|
||||||
|
sso: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { handleSubmit, defineField, errors, meta } = useForm({
|
const { handleSubmit, defineField, errors, meta } = useForm({
|
||||||
@@ -33,6 +35,25 @@ const onSubmit = handleSubmit(async (values) => {
|
|||||||
console.error('Submission failed:', error)
|
console.error('Submission failed:', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { initKeycloak, getProfile, loginSSO } = useKeycloak()
|
||||||
|
const profile = ref<any>(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initKeycloak('check-sso')
|
||||||
|
profile.value = getProfile()
|
||||||
|
console.log(profile)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSSO = (async () => {
|
||||||
|
try {
|
||||||
|
const redirect = window.location.origin + '/auth/sso'
|
||||||
|
await loginSSO({ redirectUri: redirect })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Call SSO failed:', error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -71,4 +92,8 @@ const onSubmit = handleSubmit(async (values) => {
|
|||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<Button @click="onSSO" target="_blank">
|
||||||
|
Login SSO
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -11,6 +11,7 @@ import { calculateAge } from '~/models/person'
|
|||||||
// components
|
// components
|
||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
import { InputBase, FileField as FileUpload } from '~/components/pub/my-ui/form'
|
import { InputBase, FileField as FileUpload } from '~/components/pub/my-ui/form'
|
||||||
|
import { Skeleton } from '~/components/pub/ui/skeleton'
|
||||||
import { SelectBirthPlace } from '~/components/app/person/fields'
|
import { SelectBirthPlace } from '~/components/app/person/fields'
|
||||||
import {
|
import {
|
||||||
InputName,
|
InputName,
|
||||||
@@ -60,10 +61,20 @@ interface PatientFormInput {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReadonly: boolean
|
isReadonly: boolean
|
||||||
|
languageOptions?: { value: string; label: string }[]
|
||||||
|
ethnicOptions?: { value: string; label: string }[]
|
||||||
initialValues?: PatientFormInput
|
initialValues?: PatientFormInput
|
||||||
|
mode?: 'add' | 'edit'
|
||||||
|
identityFileUrl?: string
|
||||||
|
familyCardFileUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'preview', url: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const formSchema = toTypedSchema(PatientSchema)
|
const formSchema = toTypedSchema(PatientSchema)
|
||||||
|
|
||||||
const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm<FormData>({
|
const { values, resetForm, setValues, setFieldValue, validate, setFieldError } = useForm<FormData>({
|
||||||
@@ -208,6 +219,7 @@ watch(
|
|||||||
label="Suku"
|
label="Suku"
|
||||||
placeholder="Pilih suku bangsa"
|
placeholder="Pilih suku bangsa"
|
||||||
:is-disabled="isReadonly || values.nationality !== 'WNI'"
|
:is-disabled="isReadonly || values.nationality !== 'WNI'"
|
||||||
|
:items="ethnicOptions || []"
|
||||||
/>
|
/>
|
||||||
<SelectLanguage
|
<SelectLanguage
|
||||||
field-name="language"
|
field-name="language"
|
||||||
@@ -215,6 +227,7 @@ watch(
|
|||||||
placeholder="Pilih preferensi bahasa"
|
placeholder="Pilih preferensi bahasa"
|
||||||
is-required
|
is-required
|
||||||
:is-disabled="isReadonly"
|
:is-disabled="isReadonly"
|
||||||
|
:items="languageOptions || []"
|
||||||
/>
|
/>
|
||||||
<SelectReligion
|
<SelectReligion
|
||||||
field-name="religion"
|
field-name="religion"
|
||||||
@@ -256,22 +269,112 @@ watch(
|
|||||||
:col-count="2"
|
:col-count="2"
|
||||||
:cell-flex="false"
|
:cell-flex="false"
|
||||||
>
|
>
|
||||||
<FileUpload
|
<div class="flex flex-col gap-3">
|
||||||
field-name="identityCardFile"
|
<FileUpload
|
||||||
label="Dokumen KTP"
|
field-name="residentIdentityFile"
|
||||||
placeholder="Unggah scan dokumen KTP"
|
label="Dokumen KTP"
|
||||||
:accept="['pdf', 'jpg', 'png']"
|
placeholder="Unggah scan dokumen KTP"
|
||||||
:max-size-mb="1"
|
:accept="['pdf', 'jpg', 'png']"
|
||||||
:is-disabled="isReadonly"
|
:max-size-mb="1"
|
||||||
/>
|
:is-disabled="isReadonly"
|
||||||
<FileUpload
|
/>
|
||||||
field-name="familyCardFile"
|
<!-- Embed Preview Dokumen KTP (mode edit) -->
|
||||||
label="Dokumen KK"
|
<div
|
||||||
placeholder="Unggah scan dokumen KK"
|
v-if="mode === 'edit'"
|
||||||
:accept="['pdf', 'jpg', 'png']"
|
class="flex flex-col gap-1"
|
||||||
:max-size-mb="1"
|
>
|
||||||
:is-disabled="isReadonly"
|
<span class="text-xs text-muted-foreground">Dokumen Tersimpan</span>
|
||||||
/>
|
<div
|
||||||
|
class="relative h-28 w-48 cursor-pointer overflow-hidden rounded-md border bg-muted"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-60': !identityFileUrl }"
|
||||||
|
@click="identityFileUrl && emit('preview', identityFileUrl)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="identityFileUrl"
|
||||||
|
:src="identityFileUrl"
|
||||||
|
alt="Dokumen KTP"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
v-else
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
|
||||||
|
:class="identityFileUrl ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="identityFileUrl"
|
||||||
|
class="flex flex-col items-center gap-1 text-white"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-eye h-5 w-5" />
|
||||||
|
<span class="text-xs font-medium">Lihat Dokumen</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center gap-1 text-white/70"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-info h-5 w-5" />
|
||||||
|
<span class="text-xs font-medium">No Data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<FileUpload
|
||||||
|
field-name="familyIdentityFile"
|
||||||
|
label="Dokumen KK"
|
||||||
|
placeholder="Unggah scan dokumen KK"
|
||||||
|
:accept="['pdf', 'jpg', 'png']"
|
||||||
|
:max-size-mb="1"
|
||||||
|
:is-disabled="isReadonly"
|
||||||
|
/>
|
||||||
|
<!-- Embed Preview Dokumen KK (mode edit) -->
|
||||||
|
<div
|
||||||
|
v-if="mode === 'edit'"
|
||||||
|
class="flex flex-col gap-1"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-muted-foreground">Dokumen Tersimpan</span>
|
||||||
|
<div
|
||||||
|
class="relative h-28 w-48 cursor-pointer overflow-hidden rounded-md border bg-muted"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-60': !familyCardFileUrl }"
|
||||||
|
@click="familyCardFileUrl && emit('preview', familyCardFileUrl)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="familyCardFileUrl"
|
||||||
|
:src="familyCardFileUrl"
|
||||||
|
alt="Dokumen KK"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
v-else
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
|
||||||
|
:class="familyCardFileUrl ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="familyCardFileUrl"
|
||||||
|
class="flex flex-col items-center gap-1 text-white"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-eye h-5 w-5" />
|
||||||
|
<span class="text-xs font-medium">Lihat Dokumen</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center gap-1 text-white/70"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-info h-5 w-5" />
|
||||||
|
<span class="text-xs font-medium">No Data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DE.Block>
|
</DE.Block>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -6,6 +6,7 @@ import { cn } from '~/lib/utils'
|
|||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
items: { value: string; label: string }[]
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -28,23 +29,23 @@ const {
|
|||||||
labelClass,
|
labelClass,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ethnicOptions = [
|
// const ethnicOptions = [
|
||||||
{ label: 'Tidak diketahui', value: 'unknown', priority: 1 },
|
// { label: 'Tidak diketahui', value: 'unknown', priority: 1 },
|
||||||
{ label: 'Jawa', value: 'jawa' },
|
// { label: 'Jawa', value: 'jawa' },
|
||||||
{ label: 'Sunda', value: 'sunda' },
|
// { label: 'Sunda', value: 'sunda' },
|
||||||
{ label: 'Batak', value: 'batak' },
|
// { label: 'Batak', value: 'batak' },
|
||||||
{ label: 'Betawi', value: 'betawi' },
|
// { label: 'Betawi', value: 'betawi' },
|
||||||
{ label: 'Minangkabau', value: 'minangkabau' },
|
// { label: 'Minangkabau', value: 'minangkabau' },
|
||||||
{ label: 'Bugis', value: 'bugis' },
|
// { label: 'Bugis', value: 'bugis' },
|
||||||
{ label: 'Madura', value: 'madura' },
|
// { label: 'Madura', value: 'madura' },
|
||||||
{ label: 'Banjar', value: 'banjar' },
|
// { label: 'Banjar', value: 'banjar' },
|
||||||
{ label: 'Bali', value: 'bali' },
|
// { label: 'Bali', value: 'bali' },
|
||||||
{ label: 'Dayak', value: 'dayak' },
|
// { label: 'Dayak', value: 'dayak' },
|
||||||
{ label: 'Aceh', value: 'aceh' },
|
// { label: 'Aceh', value: 'aceh' },
|
||||||
{ label: 'Sasak', value: 'sasak' },
|
// { label: 'Sasak', value: 'sasak' },
|
||||||
{ label: 'Papua', value: 'papua' },
|
// { label: 'Papua', value: 'papua' },
|
||||||
{ label: 'Lainnya', value: 'lainnya', priority: -100 },
|
// { label: 'Lainnya', value: 'lainnya', priority: -100 },
|
||||||
]
|
// ]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -70,7 +71,7 @@ const ethnicOptions = [
|
|||||||
<Combobox
|
<Combobox
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="ethnicOptions"
|
:items="items"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:is-disabled="isDisabled"
|
:is-disabled="isDisabled"
|
||||||
search-placeholder="Cari..."
|
search-placeholder="Cari..."
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { cn } from '~/lib/utils'
|
|||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
items: { value: string; label: string }[]
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
label?: string
|
label?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -29,15 +30,15 @@ const {
|
|||||||
labelClass,
|
labelClass,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const langOptions = [
|
// const langOptions = [
|
||||||
{ label: 'Bahasa Indonesia', value: 'id', priority: 1 },
|
// { label: 'Bahasa Indonesia', value: 'id', priority: 1 },
|
||||||
{ label: 'Bahasa Jawa', value: 'jawa' },
|
// { label: 'Bahasa Jawa', value: 'jawa' },
|
||||||
{ label: 'Bahasa Sunda', value: 'sunda' },
|
// { label: 'Bahasa Sunda', value: 'sunda' },
|
||||||
{ label: 'Bahasa Bali', value: 'bali' },
|
// { label: 'Bahasa Bali', value: 'bali' },
|
||||||
{ label: 'Bahasa Jaksel', value: 'jaksel' },
|
// { label: 'Bahasa Jaksel', value: 'jaksel' },
|
||||||
{ label: 'Bahasa Inggris', value: 'en' },
|
// { label: 'Bahasa Inggris', value: 'en' },
|
||||||
{ label: 'Tidak Diketahui', value: 'unknown', priority: 100 },
|
// { label: 'Tidak Diketahui', value: 'unknown', priority: 100 },
|
||||||
]
|
// ]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,7 +65,7 @@ const langOptions = [
|
|||||||
:is-disabled="isDisabled"
|
:is-disabled="isDisabled"
|
||||||
:id="fieldName"
|
:id="fieldName"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:items="langOptions"
|
:items="items"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:preserve-order="false"
|
:preserve-order="false"
|
||||||
:class="
|
:class="
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'back'): void
|
(e: 'back'): void
|
||||||
(e: 'edit'): void
|
(e: 'edit'): void
|
||||||
|
(e: 'preview', url: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
@@ -70,6 +71,14 @@ const patientAge = computed(() => {
|
|||||||
}
|
}
|
||||||
return calculateAge(props.patient.person.birthDate)
|
return calculateAge(props.patient.person.birthDate)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const familiyCardFile = computed(() => {
|
||||||
|
return props.patient.person.familyIdentityFileUrl || ''
|
||||||
|
})
|
||||||
|
const identityFile = computed(() => {
|
||||||
|
return props.patient.person.residentIdentityFileUrl || ''
|
||||||
|
})
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Lifecycle Hooks
|
// #region Lifecycle Hooks
|
||||||
@@ -166,13 +175,85 @@ function onNavigate(type: ClickType) {
|
|||||||
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
<AccordionTrigger>{{ section }}</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Dokumen KTP -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="text-sm text-muted-foreground">Dokumen KTP</span>
|
<span class="text-sm text-muted-foreground">Dokumen KTP</span>
|
||||||
<Skeleton class="h-32 w-64 rounded-md" />
|
<div
|
||||||
|
class="relative h-32 w-64 cursor-pointer overflow-hidden rounded-md border bg-muted"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-60': !identityFile }"
|
||||||
|
@click="identityFile && emit('preview', identityFile)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="identityFile"
|
||||||
|
:src="identityFile"
|
||||||
|
alt="Dokumen KTP"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
v-else
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
|
||||||
|
:class="identityFile ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="identityFile"
|
||||||
|
class="flex flex-col items-center gap-1 text-white"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-eye h-6 w-6" />
|
||||||
|
<span class="text-sm font-medium">Lihat Dokumen</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center gap-1 text-white/70"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-info h-6 w-6" />
|
||||||
|
<span class="text-sm font-medium">No Data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Dokumen KK -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="text-sm text-muted-foreground">Dokumen KK</span>
|
<span class="text-sm text-muted-foreground">Dokumen KK</span>
|
||||||
<Skeleton class="h-32 w-64 rounded-md" />
|
<div
|
||||||
|
class="relative h-32 w-64 cursor-pointer overflow-hidden rounded-md border bg-muted"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-60': !familiyCardFile }"
|
||||||
|
@click="familiyCardFile && emit('preview', familiyCardFile)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="familiyCardFile"
|
||||||
|
:src="familiyCardFile"
|
||||||
|
alt="Dokumen KK"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
v-else
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black/50 transition-opacity"
|
||||||
|
:class="familiyCardFile ? 'opacity-0 hover:opacity-100' : 'opacity-100'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="familiyCardFile"
|
||||||
|
class="flex flex-col items-center gap-1 text-white"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-eye h-6 w-6" />
|
||||||
|
<span class="text-sm font-medium">Lihat Dokumen</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center gap-1 text-white/70"
|
||||||
|
>
|
||||||
|
<span class="i-lucide-info h-6 w-6" />
|
||||||
|
<span class="text-sm font-medium">No Data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import type { Person } from '~/models/person'
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||||
|
|
||||||
import { getPatientDetail } from '~/services/patient.service'
|
import { getPatientDetail } from '~/services/patient.service'
|
||||||
|
|
||||||
@@ -17,6 +19,9 @@ const props = defineProps<{
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region State & Computed
|
// #region State & Computed
|
||||||
|
const isDocPreviewDialogOpen = ref<boolean>(false)
|
||||||
|
const docPreviewUrl = ref<string>('')
|
||||||
|
|
||||||
const patient = ref(
|
const patient = ref(
|
||||||
withBase<PatientEntity>({
|
withBase<PatientEntity>({
|
||||||
person: {} as Person,
|
person: {} as Person,
|
||||||
@@ -80,6 +85,19 @@ async function onEdit() {
|
|||||||
:patient="patient"
|
:patient="patient"
|
||||||
@back="onBack"
|
@back="onBack"
|
||||||
@edit="onEdit"
|
@edit="onEdit"
|
||||||
|
@preview="
|
||||||
|
(url: string) => {
|
||||||
|
docPreviewUrl = url
|
||||||
|
isDocPreviewDialogOpen = true
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isDocPreviewDialogOpen"
|
||||||
|
title="Preview Dokumen"
|
||||||
|
size="2xl"
|
||||||
|
>
|
||||||
|
<DocPreviewDialog :link="docPreviewUrl" />
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +17,8 @@ import AppPersonAddressEntryFormRelative from '~/components/app/person-address/e
|
|||||||
import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue'
|
import AppPersonFamilyParentsForm from '~/components/app/person/family-parents-form.vue'
|
||||||
import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue'
|
import AppPersonContactEntryForm from '~/components/app/person-contact/entry-form.vue'
|
||||||
import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue'
|
import AppPersonRelativeEntryForm from '~/components/app/person-relative/entry-form.vue'
|
||||||
|
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||||
|
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
import { format, parseISO } from 'date-fns'
|
import { format, parseISO } from 'date-fns'
|
||||||
@@ -24,6 +26,8 @@ import { id as localeID } from 'date-fns/locale'
|
|||||||
|
|
||||||
// services
|
// services
|
||||||
import { getPatientDetail, uploadAttachment } from '~/services/patient.service'
|
import { getPatientDetail, uploadAttachment } from '~/services/patient.service'
|
||||||
|
import { getValueLabelList as getEthnicOpts } from '~/services/ethnic.service'
|
||||||
|
import { getValueLabelList as getLanguageOpts } from '~/services/language.service'
|
||||||
|
|
||||||
import { isReadonly, isProcessing, handleActionSave, handleActionEdit } from '~/handlers/patient.handler'
|
import { isReadonly, isProcessing, handleActionSave, handleActionEdit } from '~/handlers/patient.handler'
|
||||||
|
|
||||||
@@ -47,6 +51,23 @@ const props = defineProps<{
|
|||||||
const residentIdentityFile = ref<File>()
|
const residentIdentityFile = ref<File>()
|
||||||
const familyCardFile = ref<File>()
|
const familyCardFile = ref<File>()
|
||||||
|
|
||||||
|
// Dialog preview dokumen
|
||||||
|
const isDocPreviewDialogOpen = ref<boolean>(false)
|
||||||
|
const docPreviewUrl = ref<string>('')
|
||||||
|
|
||||||
|
// Computed untuk URL dokumen tersimpan
|
||||||
|
const identityFileUrl = computed(() => {
|
||||||
|
return patientDetail.value.person?.residentIdentityFileUrl || ''
|
||||||
|
})
|
||||||
|
const familyCardFileUrl = computed(() => {
|
||||||
|
return patientDetail.value.person?.familyIdentityFileUrl || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function handlePreviewDoc(url: string) {
|
||||||
|
docPreviewUrl.value = url
|
||||||
|
isDocPreviewDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// form related state
|
// form related state
|
||||||
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
const personAddressForm = ref<ExposedForm<any> | null>(null)
|
||||||
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
|
const personAddressRelativeForm = ref<ExposedForm<any> | null>(null)
|
||||||
@@ -58,6 +79,9 @@ const personPatientForm = ref<ExposedForm<any> | null>(null)
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region State & Computed
|
// #region State & Computed
|
||||||
|
const ethnicOptions = ref<{ value: string; label: string }[]>([])
|
||||||
|
const languageOptions = ref<{ value: string; label: string }[]>([])
|
||||||
|
|
||||||
const patientDetail = ref(
|
const patientDetail = ref(
|
||||||
withBase<PatientEntity>({
|
withBase<PatientEntity>({
|
||||||
person: {} as Person,
|
person: {} as Person,
|
||||||
@@ -223,6 +247,13 @@ const responsibleFormInitialValues = computed(() => {
|
|||||||
|
|
||||||
// #region Lifecycle Hooks
|
// #region Lifecycle Hooks
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
const optsReq = {
|
||||||
|
'page-no-limit': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
ethnicOptions.value = await getEthnicOpts(optsReq, true)
|
||||||
|
languageOptions.value = await getLanguageOpts(optsReq, true)
|
||||||
|
|
||||||
// if edit mode, fetch patient detail
|
// if edit mode, fetch patient detail
|
||||||
if (props.mode === 'edit' && props.patientId) {
|
if (props.mode === 'edit' && props.patientId) {
|
||||||
await loadInitData(props.patientId)
|
await loadInitData(props.patientId)
|
||||||
@@ -309,7 +340,9 @@ async function handleActionClick(eventType: string) {
|
|||||||
const patient: Patient = await composeFormData()
|
const patient: Patient = await composeFormData()
|
||||||
|
|
||||||
let createdPatientId = 0
|
let createdPatientId = 0
|
||||||
|
let createdPersonId = 0
|
||||||
let response: any
|
let response: any
|
||||||
|
|
||||||
// return
|
// return
|
||||||
if (props.mode === 'edit' && props.patientId) {
|
if (props.mode === 'edit' && props.patientId) {
|
||||||
response = await handleActionEdit(
|
response = await handleActionEdit(
|
||||||
@@ -330,13 +363,15 @@ async function handleActionClick(eventType: string) {
|
|||||||
|
|
||||||
const data = (response?.body?.data ?? null) as PatientBase | null
|
const data = (response?.body?.data ?? null) as PatientBase | null
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
createdPatientId = data.id
|
createdPatientId = data.id
|
||||||
|
createdPersonId = data.person_id!
|
||||||
|
|
||||||
if (residentIdentityFile.value) {
|
if (residentIdentityFile.value) {
|
||||||
void uploadAttachment(residentIdentityFile.value, createdPatientId, 'ktp')
|
void uploadAttachment(residentIdentityFile.value, createdPersonId, 'ktp')
|
||||||
}
|
}
|
||||||
if (familyCardFile.value) {
|
if (familyCardFile.value) {
|
||||||
void uploadAttachment(familyCardFile.value, createdPatientId, 'kk')
|
void uploadAttachment(familyCardFile.value, createdPersonId, 'kk')
|
||||||
}
|
}
|
||||||
|
|
||||||
// If has callback provided redirect to callback with patientData
|
// If has callback provided redirect to callback with patientData
|
||||||
@@ -435,10 +470,16 @@ watch(
|
|||||||
{{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }}
|
{{ mode === 'edit' ? 'Edit Pasien' : 'Tambah Pasien' }}
|
||||||
</div>
|
</div>
|
||||||
<AppPatientEntryForm
|
<AppPatientEntryForm
|
||||||
:key="`patient-${formKey}`"
|
|
||||||
ref="personPatientForm"
|
ref="personPatientForm"
|
||||||
|
:key="`patient-${formKey}`"
|
||||||
:is-readonly="isProcessing || isReadonly"
|
:is-readonly="isProcessing || isReadonly"
|
||||||
:initial-values="patientFormInitialValues"
|
:initial-values="patientFormInitialValues"
|
||||||
|
:language-options="languageOptions"
|
||||||
|
:ethnic-options="ethnicOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:identity-file-url="identityFileUrl"
|
||||||
|
:family-card-file-url="familyCardFileUrl"
|
||||||
|
@preview="handlePreviewDoc"
|
||||||
/>
|
/>
|
||||||
<div class="h-6"></div>
|
<div class="h-6"></div>
|
||||||
<AppPersonAddressEntryForm
|
<AppPersonAddressEntryForm
|
||||||
@@ -483,6 +524,15 @@ watch(
|
|||||||
<div class="my-2 flex justify-end py-2">
|
<div class="my-2 flex justify-end py-2">
|
||||||
<Action @click="handleActionClick" />
|
<Action @click="handleActionClick" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog Preview Dokumen -->
|
||||||
|
<Dialog
|
||||||
|
v-model:open="isDocPreviewDialogOpen"
|
||||||
|
title="Preview Dokumen"
|
||||||
|
size="2xl"
|
||||||
|
>
|
||||||
|
<DocPreviewDialog :link="docPreviewUrl" />
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSidebar } from '~/components/pub/ui/sidebar'
|
import { useSidebar } from '~/components/pub/ui/sidebar'
|
||||||
|
import { useKeycloak } from "~/composables/useKeycloack"
|
||||||
|
|
||||||
// defineProps<{
|
// defineProps<{
|
||||||
// user: {
|
// user: {
|
||||||
@@ -11,11 +12,15 @@ import { useSidebar } from '~/components/pub/ui/sidebar'
|
|||||||
|
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar()
|
||||||
const { user, logout, setActiveRole, getActiveRole } = useUserStore()
|
const { user, logout, setActiveRole, getActiveRole } = useUserStore()
|
||||||
|
const { initKeycloak, logoutSSO } = useKeycloak()
|
||||||
// const userStore = useUserStore().user
|
// const userStore = useUserStore().user
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
|
initKeycloak('check-sso')
|
||||||
|
|
||||||
navigateTo('/auth/login')
|
navigateTo('/auth/login')
|
||||||
logout()
|
logout()
|
||||||
|
logoutSSO(window.location.origin + '/auth/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showModalTheme = ref(false)
|
const showModalTheme = ref(false)
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import Keycloak from "keycloak-js";
|
||||||
|
import { ref, computed, onBeforeMount } from "vue";
|
||||||
|
|
||||||
|
let kc: any | null = null;
|
||||||
|
|
||||||
|
const initialized = ref(false);
|
||||||
|
const authenticated = ref(false);
|
||||||
|
const token = ref<string | null>(null);
|
||||||
|
const profile = ref<any>(null);
|
||||||
|
|
||||||
|
export function useKeycloak() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const initKeycloak = async (onLoad: "login-required" | "check-sso" = "check-sso") => {
|
||||||
|
if (kc) return kc;
|
||||||
|
kc = new Keycloak({
|
||||||
|
url: config.public.KEYCLOAK_URL,
|
||||||
|
realm: config.public.KEYCLOAK_REALM,
|
||||||
|
clientId: config.public.CLIENT_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const initOptions = {
|
||||||
|
onLoad,
|
||||||
|
promiseType: "native" as const,
|
||||||
|
pkceMethod: "S256" as const,
|
||||||
|
};
|
||||||
|
console.log(kc.url)
|
||||||
|
authenticated.value = await kc.init(initOptions);
|
||||||
|
initialized.value = true;
|
||||||
|
token.value = kc.token ?? null;
|
||||||
|
if (authenticated.value) {
|
||||||
|
try {
|
||||||
|
profile.value = await kc.loadUserProfile();
|
||||||
|
} catch (e) {
|
||||||
|
profile.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// automatically update token in background
|
||||||
|
kc.onTokenExpired = async () => {
|
||||||
|
try {
|
||||||
|
const refreshed = await kc.updateToken(30);
|
||||||
|
token.value = kc?.token ?? null;
|
||||||
|
if (!refreshed) {
|
||||||
|
// token not refreshed but still valid
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to refresh token", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return kc;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(authenticated)
|
||||||
|
console.error("Keycloak init xyz failed", err);
|
||||||
|
initialized.value = true;
|
||||||
|
authenticated.value = false;
|
||||||
|
return kc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginSSO = (options?: Keycloak.KeycloakLoginOptions) => {
|
||||||
|
if (!kc) throw new Error("Keycloak not initialized");
|
||||||
|
return kc.login(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoutSSO = (redirectUri?: string) => {
|
||||||
|
if (!kc) throw new Error("Keycloak not initialized");
|
||||||
|
return kc.logout({ redirectUri });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToken = () => token.value;
|
||||||
|
const isAuthenticated = computed(() => authenticated.value);
|
||||||
|
const getProfile = () => profile.value;
|
||||||
|
|
||||||
|
// init on client automatically
|
||||||
|
onBeforeMount(() => {
|
||||||
|
// try check-sso silently
|
||||||
|
if (!initialized.value) initKeycloak("check-sso");
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiErrors = ref<Record<string, string>>({})
|
||||||
|
const { login } = useUserStore()
|
||||||
|
|
||||||
|
const getResponse = async () => {
|
||||||
|
console.log("=================== onto login fes!!! ===================")
|
||||||
|
const params = {
|
||||||
|
token: token.value,
|
||||||
|
user: profile.value
|
||||||
|
}
|
||||||
|
const result = await xfetch('/api/v1/authentication/login-fes', 'POST', {
|
||||||
|
data: params,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const { data: rawdata, meta } = result.body
|
||||||
|
if (meta.status === 'verified') {
|
||||||
|
login(rawdata)
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (result.errors) {
|
||||||
|
Object.entries(result.errors).forEach(
|
||||||
|
([field, errorInfo]: [string, any]) => (apiErrors.value[field] = errorInfo.message),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
apiErrors.value.general = result.error?.message || result.message || 'Login failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initKeycloak,
|
||||||
|
loginSSO,
|
||||||
|
logoutSSO,
|
||||||
|
getToken,
|
||||||
|
isAuthenticated,
|
||||||
|
getProfile,
|
||||||
|
getResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
export default defineNuxtRouteMiddleware((to) => {
|
import { useKeycloak } from "~/composables/useKeycloack"
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
if (to.meta.public) return
|
if (to.meta.public) return
|
||||||
|
// if (!to.meta?.requiresAuth) return;
|
||||||
|
|
||||||
const { $pinia } = useNuxtApp()
|
const { $pinia } = useNuxtApp()
|
||||||
|
|
||||||
|
const { initKeycloak, isAuthenticated, getResponse} = useKeycloak(); // global composable
|
||||||
|
await initKeycloak("check-sso");
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const userStore = useUserStore($pinia)
|
const userStore = useUserStore($pinia)
|
||||||
if (!userStore.isAuthenticated) {
|
if (!userStore.isAuthenticated && !isAuthenticated.value) {
|
||||||
return navigateTo('/auth/login')
|
return navigateTo('/auth/login')
|
||||||
|
} else {
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
await getResponse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Base, genBase } from "./_base"
|
import { type Base, genBase } from './_base'
|
||||||
|
|
||||||
export interface Ethnic extends Base {
|
export interface Ethnic extends Base {
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { type Base, genBase } from "./_base"
|
import { type Base, genBase } from './_base'
|
||||||
|
|
||||||
export interface Language extends Base {
|
export interface Language extends Base {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genMcuSrc(): Language {
|
export function genLanguage(): Language {
|
||||||
return {
|
return {
|
||||||
...genBase(),
|
...genBase(),
|
||||||
code: '',
|
code: '',
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useKeycloak } from "~/composables/useKeycloack"
|
||||||
|
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const { initKeycloak, isAuthenticated, getResponse } = useKeycloak()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// Initialize Keycloak with login-required to ensure tokens set (Keycloak will process the code/state returned)
|
||||||
|
await initKeycloak('login-required')
|
||||||
|
// small delay to allow token propagation
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
await getResponse()
|
||||||
|
} else {
|
||||||
|
return navigateTo('/auth/login')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err?.message || String(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="max-width:720px;margin:40px auto">
|
||||||
|
<h2>Processing login...</h2>
|
||||||
|
<p v-if="error">Terjadi error: {{ error }}</p>
|
||||||
|
<p v-else>Mohon tunggu, sedang memproses otentikasi dan mengarahkan Anda ...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Base
|
||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { Ethnic } from '~/models/ethnic'
|
||||||
|
|
||||||
|
const path = '/api/v1/ethnic'
|
||||||
|
const name = 'ethnic'
|
||||||
|
|
||||||
|
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,
|
||||||
|
useCodeAsValue = false,
|
||||||
|
): 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: Ethnic) => ({
|
||||||
|
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
|
||||||
|
label: item.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Base
|
||||||
|
import * as base from './_crud-base'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { Language } from '~/models/language'
|
||||||
|
|
||||||
|
const path = '/api/v1/language'
|
||||||
|
const name = 'language'
|
||||||
|
|
||||||
|
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,
|
||||||
|
useCodeAsValue = false,
|
||||||
|
): 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: Language) => ({
|
||||||
|
value: useCodeAsValue ? item.code : item.id ? Number(item.id) : item.code,
|
||||||
|
label: item.name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ export async function removePatient(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadAttachment(file: File, userId: number, key: UploadCodeKey) {
|
export async function uploadAttachment(file: File, personId: number, key: UploadCodeKey) {
|
||||||
try {
|
try {
|
||||||
const resolvedKey = uploadCode[key]
|
const resolvedKey = uploadCode[key]
|
||||||
if (!resolvedKey) {
|
if (!resolvedKey) {
|
||||||
@@ -110,11 +110,14 @@ export async function uploadAttachment(file: File, userId: number, key: UploadCo
|
|||||||
|
|
||||||
// siapkan form-data body
|
// siapkan form-data body
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('code', resolvedKey)
|
|
||||||
formData.append('content', file)
|
formData.append('content', file)
|
||||||
|
formData.append('entityType_code', 'person')
|
||||||
|
formData.append('ref_id', String(personId))
|
||||||
|
// formData.append('upload_employee_id', String(userId))
|
||||||
|
formData.append('type_code', resolvedKey)
|
||||||
|
|
||||||
// kirim via xfetch
|
// kirim via xfetch
|
||||||
const resp = await xfetch(`${mainUrl}/${userId}/upload`, 'POST', formData)
|
const resp = await xfetch(`/api/v1/upload-file`, 'POST', formData)
|
||||||
|
|
||||||
// struktur hasil sama seperti patchPatient
|
// struktur hasil sama seperti patchPatient
|
||||||
const result: any = {}
|
const result: any = {}
|
||||||
|
|||||||
+26
-10
@@ -6,9 +6,25 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
||||||
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
||||||
|
//SSO
|
||||||
|
X_AP_CODE: process.env.X_AP_CODE || 'rssa-sso',
|
||||||
|
X_AP_SECRET_KEY: process.env.X_AP_SECRET_KEY || 'sapiperah',
|
||||||
|
SSO_CONFIRM_URL: process.env.SSO_CONFIRM_URL || 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/userinfo',
|
||||||
|
KEYCLOAK_LOGOUT_REDIRECT: process.env.KEYCLOAK_LOGOUT_REDIRECT || 'http://localhost:3000',
|
||||||
|
KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'sandbox',
|
||||||
|
KEYCLOAK_URL: process.env.KEYCLOAK_URL || 'https://auth.dev.rssa.id/',
|
||||||
|
CLIENT_ID: process.env.CLIENT_ID || 'portal-simrs-new',
|
||||||
public: {
|
public: {
|
||||||
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
API_ORIGIN: process.env.NUXT_API_ORIGIN || 'http://localhost:3000',
|
||||||
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
VCLAIM_SWAGGER: process.env.NUXT_API_VCLAIM_SWAGGER || 'http://localhost:3000',
|
||||||
|
//SSO
|
||||||
|
X_AP_CODE: process.env.X_AP_CODE || 'rssa-sso',
|
||||||
|
X_AP_SECRET_KEY: process.env.X_AP_SECRET_KEY || 'sapiperah',
|
||||||
|
SSO_CONFIRM_URL: process.env.SSO_CONFIRM_URL || 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/userinfo',
|
||||||
|
KEYCLOAK_LOGOUT_REDIRECT: process.env.KEYCLOAK_LOGOUT_REDIRECT || 'http://localhost:3000',
|
||||||
|
KEYCLOAK_REALM: process.env.KEYCLOAK_REALM || 'sandbox',
|
||||||
|
KEYCLOAK_URL: process.env.KEYCLOAK_URL || 'https://auth.dev.rssa.id/',
|
||||||
|
CLIENT_ID: process.env.CLIENT_ID || 'portal-simrs-new',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -48,21 +64,11 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
css: ['@unocss/reset/tailwind.css', '~/assets/css/main.css'],
|
css: ['@unocss/reset/tailwind.css', '~/assets/css/main.css'],
|
||||||
|
|
||||||
colorMode: {
|
|
||||||
classSuffix: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
features: {
|
features: {
|
||||||
// For UnoCSS
|
// For UnoCSS
|
||||||
inlineStyles: false,
|
inlineStyles: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
eslint: {
|
|
||||||
config: {
|
|
||||||
standalone: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
imports: {
|
imports: {
|
||||||
dirs: ['./app/lib'],
|
dirs: ['./app/lib'],
|
||||||
},
|
},
|
||||||
@@ -71,5 +77,15 @@ export default defineNuxtConfig({
|
|||||||
componentDir: './app/components/pub/ui',
|
componentDir: './app/components/pub/ui',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
colorMode: {
|
||||||
|
classSuffix: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
standalone: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
})
|
})
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "eslint --fix ."
|
"format": "eslint --fix ."
|
||||||
},
|
},
|
||||||
|
"main": "./lib/keycloak.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify-json/lucide": "^1.2.30",
|
"@iconify-json/lucide": "^1.2.30",
|
||||||
"@iconify-json/radix-icons": "^1.2.2",
|
"@iconify-json/radix-icons": "^1.2.2",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"embla-carousel-vue": "^8.5.2",
|
"embla-carousel-vue": "^8.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"h3": "^1.15.4",
|
"h3": "^1.15.4",
|
||||||
|
"nuxt-openid-connect": "^0.8.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -53,7 +55,9 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-format": "^1.0.1",
|
"eslint-plugin-format": "^1.0.1",
|
||||||
"happy-dom": "^18.0.1",
|
"happy-dom": "^18.0.1",
|
||||||
|
"keycloak-js": "^26.2.1",
|
||||||
"lucide-vue-next": "^0.482.0",
|
"lucide-vue-next": "^0.482.0",
|
||||||
|
"next-auth": "~4.21.1",
|
||||||
"nuxt": "^4.0.3",
|
"nuxt": "^4.0.3",
|
||||||
"playwright-core": "^1.54.2",
|
"playwright-core": "^1.54.2",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { getRequestURL, readBody, setCookie } from 'h3'
|
||||||
|
|
||||||
|
// Function to verify JWT token with the userinfo endpoint
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody(event)
|
||||||
|
const url = getRequestURL(event)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
const apiSSOConfirm = config.public.SSO_CONFIRM_URL
|
||||||
|
const token = 'Bearer ' + body.data.token
|
||||||
|
|
||||||
|
const res_sso = await fetch(apiSSOConfirm,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': token,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(res_sso)
|
||||||
|
if (res_sso.status === 200) {
|
||||||
|
const apiOrigin = config.public.API_ORIGIN
|
||||||
|
|
||||||
|
const cleanOrigin = apiOrigin.replace(/\/+$/, '')
|
||||||
|
const cleanPath = url.pathname.replace(/^\/api\//, '').replace(/^\/+/, '')
|
||||||
|
const externalUrl = `${cleanOrigin}/${cleanPath}${url.search}`
|
||||||
|
|
||||||
|
const resp = await fetch(externalUrl,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: body.data.user.username,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-AuthPartner-Code': config.public.X_AP_CODE,
|
||||||
|
'X-AuthPartner-SecretKey': config.public.X_AP_SECRET_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resp.status === 200) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (data?.data?.accessToken) {
|
||||||
|
setCookie(event, 'authentication', data.data.accessToken, {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
maxAge: 60 * 60 * 24,
|
||||||
|
})
|
||||||
|
|
||||||
|
delete data.data.accessToken
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(await resp.text(), {
|
||||||
|
status: resp.status,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': resp.headers.get('content-type') || 'text/plain',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(await res_sso.text(), {
|
||||||
|
status: res_sso.status,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': res_sso.headers.get('content-type') || 'text/plain',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user