Merge branch 'dev' of github.com:dikstub-rssa/simrs-fe into feat/education-assessment-79
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefExportNav, 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 Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.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'
|
||||
import FilterDialog from '~/components/pub/my-ui/nav-header/filter-dialog.vue'
|
||||
|
||||
// #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
|
||||
isFormEntryDialogOpen.value = true
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
|
||||
const refExportNav: RefExportNav = {
|
||||
onExportCsv: () => {
|
||||
// open filter modal
|
||||
console.log(`Export CSV Clicked`)
|
||||
},
|
||||
}
|
||||
|
||||
const isFormEntryDialogOpen = ref(false)
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
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',
|
||||
}
|
||||
|
||||
const filterPrep: HeaderPrep = {
|
||||
title: "Surat Kontrol",
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
// #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
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiltering() {
|
||||
console.log('Confirmed action: Filter')
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
console.log('Ekspor CSV dipilih')
|
||||
// tambahkan logic untuk generate CSV
|
||||
}
|
||||
|
||||
function exportExcel() {
|
||||
console.log('Ekspor Excel dipilih')
|
||||
// tambahkan logic untuk generate Excel
|
||||
}
|
||||
// #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.showProcess:
|
||||
navigateTo('https://google.com', { external: true, open: { target: '_blank' } });
|
||||
break
|
||||
|
||||
case ActionEvents.showDetail:
|
||||
isHistoryDialogOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
// Trigger confirmation modal open
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<!-- 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>
|
||||
-->
|
||||
|
||||
<FilterDialog :prep="{ ...filterPrep }"
|
||||
:ref-search-nav="refSearchNav"
|
||||
:ref-export-nav="refExportNav"
|
||||
:enable-search="false"
|
||||
:enable-date-range="false"/>
|
||||
|
||||
<AppBpjsControlLetterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
title="Filter"
|
||||
>
|
||||
<AppBpjsControlLetterFilter @submit="handleFiltering" />
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isHistoryDialogOpen"
|
||||
title="Log History Surat Kontrol">
|
||||
<AppBpjsControlLetterCommonHistoryDialog />
|
||||
</Dialog>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { format } from 'date-fns'
|
||||
import { id as localeID } from 'date-fns/locale'
|
||||
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
|
||||
import Input from '~/components/pub/ui/input/Input.vue'
|
||||
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
|
||||
import AppChemotherapyListAdmin from '~/components/app/chemotherapy/list-admin.vue'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Sample data - replace with actual API call
|
||||
import { sampleRows, type ChemotherapyData } from '~/components/app/chemotherapy/sample'
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const recId = ref(0)
|
||||
const recAction = ref('')
|
||||
const recItem = ref<any>(null)
|
||||
|
||||
const search = ref('')
|
||||
const mode = route.params.mode as string || 'admin'
|
||||
const dateRange = ref<{ from: Date | null; to: Date | null }>({
|
||||
from: null,
|
||||
to: null,
|
||||
})
|
||||
|
||||
// Format date range for display
|
||||
const dateRangeDisplay = computed(() => {
|
||||
if (dateRange.value.from && dateRange.value.to) {
|
||||
return `${format(dateRange.value.from, 'dd MMMM yyyy', { locale: localeID })} - ${format(dateRange.value.to, 'dd MMMM yyyy', { locale: localeID })}`
|
||||
}
|
||||
return '12 Agustus 2025 - 32 Agustus 2025' // Default display
|
||||
})
|
||||
|
||||
// Filter + search (client-side)
|
||||
const filtered = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return sampleRows.filter((r: ChemotherapyData) => {
|
||||
if (q) {
|
||||
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// Pagination meta
|
||||
const paginationMeta = reactive<PaginationMeta>({
|
||||
recordCount: filtered.value.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: Math.ceil(filtered.value.length / 10),
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
paginationMeta.page = page
|
||||
paginationMeta.hasNext = page < paginationMeta.totalPage
|
||||
paginationMeta.hasPrev = page > 1
|
||||
}
|
||||
|
||||
function handleFilter() {
|
||||
// TODO: Implement filter logic
|
||||
console.log('Filter clicked', { search: search.value, dateRange: dateRange.value })
|
||||
}
|
||||
|
||||
// Provide proses handler for action button
|
||||
function handleProses(rec: any) {
|
||||
// Navigate to verification page with record
|
||||
navigateTo(`/outpation-action/chemotherapy/verification?id=${rec.id}`)
|
||||
}
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case 'Process':
|
||||
navigateTo(`/outpation-action/chemotherapy/${mode}/${recId.value}/verification`)
|
||||
break
|
||||
case 'Verification':
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('proses-handler', handleProses)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-full">
|
||||
<!-- Header Section -->
|
||||
<div class="border-b p-6">
|
||||
<h1 class="text-2xl font-semibold">Administrasi Pasien Rawat Jalan Kemoterapi</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Manajemen pendaftaran serta monitoring terapi pasien tindakan rawat jalan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="flex flex-wrap items-center gap-3 border-b p-4">
|
||||
<!-- Search Input -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Cari Nama /No.RM"
|
||||
class="w-64 pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Picker -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-10 w-72 justify-start border-gray-300 bg-white text-left font-normal"
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{{ dateRangeDisplay }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-0">
|
||||
<RangeCalendar />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Filter Button -->
|
||||
<Button
|
||||
variant="outline"
|
||||
class="ml-auto border-orange-500 bg-orange-50 text-orange-600 hover:bg-orange-100"
|
||||
@click="handleFilter"
|
||||
>
|
||||
<FilterIcon class="mr-2 h-4 w-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="overflow-x-auto p-4">
|
||||
<AppChemotherapyListAdmin
|
||||
:data="filtered"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Components
|
||||
import AppChemotherapyList from '~/components/app/chemotherapy/list.vue'
|
||||
|
||||
// Samples
|
||||
import { sampleRows, type ChemotherapyData } from '~/components/app/chemotherapy/sample'
|
||||
|
||||
const search = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
|
||||
// filter + pencarian sederhana (client-side)
|
||||
const filtered = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return sampleRows.filter((r: ChemotherapyData) => {
|
||||
if (q) {
|
||||
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q) || r.dokter.toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-full">
|
||||
<div class="border-b p-6">
|
||||
<h1 class="text-2xl font-semibold">Daftar Kunjungan Rawat Jalan Kemoterapi</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Manajemen pendaftaran serta monitoring terapi pasien tindakan rawat jalan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 border-b p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="search"
|
||||
placeholder="Cari Nama / No.RM"
|
||||
class="w-64 rounded border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="dateFrom"
|
||||
type="date"
|
||||
class="rounded border px-3 py-2"
|
||||
/>
|
||||
<span class="text-sm text-gray-500">-</span>
|
||||
<input
|
||||
v-model="dateTo"
|
||||
type="date"
|
||||
class="rounded border px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="ml-auto rounded bg-orange-500 px-3 py-2 text-white hover:bg-orange-600">Filter</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto p-4">
|
||||
<AppChemotherapyList
|
||||
:data="filtered"
|
||||
:pagination-meta="{
|
||||
recordCount: 2,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: 1,
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Process from '~/components/content/encounter/process.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Process display="menu" class-code="ambulatory" sub-class-code="chemo" />
|
||||
</template>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
// Types
|
||||
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
// Components
|
||||
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
||||
import ProtocolList from '~/components/app/chemotherapy/list.protocol.vue'
|
||||
|
||||
// Services
|
||||
import { getDetail } from '~/services/encounter.service'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// activeTab selalu sinkron dengan query param
|
||||
const activeTab = computed({
|
||||
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
|
||||
set: (val: string) => {
|
||||
router.replace({ path: route.path, query: { tab: val } })
|
||||
},
|
||||
})
|
||||
|
||||
// Dummy data so AppEncounterQuickInfo can render in development/storybook
|
||||
// Replace with real API result when available (see commented fetch below)
|
||||
const data = ref<any>({
|
||||
patient: {
|
||||
number: 'RM-2025-0001',
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
birthDate: '1980-01-01T00:00:00Z',
|
||||
gender_code: 'M',
|
||||
addresses: [
|
||||
{ address: 'Jl. Contoh No.1, Jakarta' }
|
||||
],
|
||||
frontTitle: '',
|
||||
endTitle: ''
|
||||
}
|
||||
},
|
||||
visitDate: new Date().toISOString(),
|
||||
unit: { name: 'Onkologi' },
|
||||
responsible_doctor: null,
|
||||
appointment_doctor: { employee: { person: { name: 'Dr. Clara Smith', frontTitle: 'Dr.', endTitle: 'Sp.OG' } } }
|
||||
})
|
||||
|
||||
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
|
||||
const protocolRows = [
|
||||
{
|
||||
number: '1',
|
||||
tanggal: new Date().toISOString().substring(0, 10),
|
||||
siklus: 'I',
|
||||
periode: 'Siklus I',
|
||||
kehadiran: 'Hadir',
|
||||
action: '',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
tanggal: new Date().toISOString().substring(0, 10),
|
||||
siklus: 'II',
|
||||
periode: 'Siklus II',
|
||||
kehadiran: 'Tidak Hadir',
|
||||
action: '',
|
||||
},
|
||||
]
|
||||
|
||||
const paginationMeta: PaginationMeta = {
|
||||
recordCount: protocolRows.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
}
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ value: 'chemotherapy-protocol', label: 'Protokol Kemoterapi', component: ProtocolList, props: { data: protocolRows, paginationMeta } },
|
||||
{ value: 'chemotherapy-medicine', label: 'Protokol Obat Kemoterapi' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
// const id = typeof route.query.id == 'string' ? parseInt(route.query.id) : 0
|
||||
// const dataRes = await getDetail(id, {
|
||||
// includes:
|
||||
// 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
|
||||
// })
|
||||
// const dataResBody = dataRes.body ?? null
|
||||
// data.value = dataResBody?.data ?? null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-4">
|
||||
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
|
||||
</div>
|
||||
<AppEncounterQuickInfo :data="data" />
|
||||
<CompTab
|
||||
:data="tabs"
|
||||
:initial-active-tab="activeTab"
|
||||
@change-tab="activeTab = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { format } from 'date-fns'
|
||||
import { id as localeID } from 'date-fns/locale'
|
||||
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
|
||||
import Input from '~/components/pub/ui/input/Input.vue'
|
||||
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
|
||||
import AppChemotherapyListVerification from '~/components/app/chemotherapy/list-verification.vue'
|
||||
import DialogVerification from '~/components/app/chemotherapy/dialog-verification.vue'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Sample patient data - replace with actual API call
|
||||
const patientData = ref({
|
||||
noRm: 'RM21123',
|
||||
nama: 'Ahmad Sutanto',
|
||||
jenisPembayaran: 'PKS',
|
||||
noBilling: '18291822',
|
||||
tanggalLahir: '23 April 1992',
|
||||
usia: '33 tahun',
|
||||
jenisKelamin: 'Laki-Laki (L)',
|
||||
diagnosis: 'C34.9 - Karsinoma Paru',
|
||||
klinik: 'Penyakit Dalam',
|
||||
})
|
||||
|
||||
// Sample schedule data - replace with actual API call
|
||||
const scheduleData = ref([
|
||||
{
|
||||
id: 1,
|
||||
tanggalMasuk: '12 Agustus 2025',
|
||||
pjBerkasRm: 'TPP Rawat Jalan',
|
||||
dokter: 'Dr. Andreas Sutaji',
|
||||
jenisRuangan: 'Ruang Tindakan',
|
||||
jenisTindakan: 'KEMOTERAPI',
|
||||
tanggalJadwalTindakan: '-',
|
||||
status: 'belum_terverifikasi',
|
||||
tanggalPemeriksaan: '2025-08-12',
|
||||
},
|
||||
])
|
||||
|
||||
const search = ref('')
|
||||
const dateRange = ref<{ from: Date | null; to: Date | null }>({
|
||||
from: null,
|
||||
to: null,
|
||||
})
|
||||
|
||||
// Format date range for display
|
||||
const dateRangeDisplay = computed(() => {
|
||||
if (dateRange.value.from && dateRange.value.to) {
|
||||
return `${format(dateRange.value.from, 'dd MMMM yyyy', { locale: localeID })} - ${format(dateRange.value.to, 'dd MMMM yyyy', { locale: localeID })}`
|
||||
}
|
||||
return '12 Agustus 2025 - 32 Agustus 2025' // Default display
|
||||
})
|
||||
|
||||
// Filter + search (client-side)
|
||||
const filtered = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return scheduleData.value.filter((r: any) => {
|
||||
if (q) {
|
||||
return r.dokter.toLowerCase().includes(q) || patientData.value.noRm.toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// Pagination meta
|
||||
const paginationMeta = reactive<PaginationMeta>({
|
||||
recordCount: filtered.value.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: Math.ceil(filtered.value.length / 10),
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
paginationMeta.page = page
|
||||
paginationMeta.hasNext = page < paginationMeta.totalPage
|
||||
paginationMeta.hasPrev = page > 1
|
||||
}
|
||||
|
||||
function handleFilter() {
|
||||
// TODO: Implement filter logic
|
||||
console.log('Filter clicked', { search: search.value, dateRange: dateRange.value })
|
||||
}
|
||||
|
||||
// Dialog verification state
|
||||
const isDialogVerificationOpen = ref(false)
|
||||
const selectedSchedule = ref<any>(null)
|
||||
|
||||
// Provide verify handler for verify button
|
||||
function handleVerify(rec: any) {
|
||||
selectedSchedule.value = rec
|
||||
isDialogVerificationOpen.value = true
|
||||
}
|
||||
|
||||
provide('verify-handler', handleVerify)
|
||||
|
||||
function handleDialogSubmit(data: { tanggalPemeriksaan: string | undefined; jadwalTanggalPemeriksaan: string | undefined }) {
|
||||
// TODO: Implement submit logic
|
||||
console.log('Verification submitted', data)
|
||||
isDialogVerificationOpen.value = false
|
||||
// Refresh data after verification
|
||||
}
|
||||
|
||||
function handleBackToAdmin() {
|
||||
router.push('/outpation-action/chemotherapy/admin')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-full">
|
||||
<!-- Back Button -->
|
||||
<div class="mb-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex items-center gap-2 rounded-full border border-orange-400 bg-orange-50 px-3 py-1 text-sm font-medium text-orange-600 hover:bg-orange-100"
|
||||
@click="handleBackToAdmin"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Kembali ke Administrasi Kunjungan
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Data Pasien Section -->
|
||||
<div class="mb-6 rounded-md border bg-white p-4 shadow-sm">
|
||||
<h3 class="mb-4 text-lg font-semibold">Data Pasien:</h3>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">No. RM:</span>
|
||||
<span>{{ patientData.noRm }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Nama:</span>
|
||||
<span>{{ patientData.nama }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Jenis Pembayaran:</span>
|
||||
<span>{{ patientData.jenisPembayaran }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">No Billing:</span>
|
||||
<span>{{ patientData.noBilling }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Tanggal Lahir / Usia:</span>
|
||||
<span>{{ patientData.tanggalLahir }} / {{ patientData.usia }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Jenis Kelamin:</span>
|
||||
<span>{{ patientData.jenisKelamin }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Diagnosis:</span>
|
||||
<span>{{ patientData.diagnosis }}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="w-48 font-medium">Klinik:</span>
|
||||
<span>{{ patientData.klinik }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="border-b p-6">
|
||||
<h1 class="text-2xl font-semibold">Verifikasi Jadwal Pasien</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Pantau riwayat masuk, dokter penanggung jawab, dan status pasien secara real-time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="flex flex-wrap items-center gap-3 border-b p-4">
|
||||
<!-- Search Input -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Cari Nama /No.RM"
|
||||
class="w-64 pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Picker -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-10 w-72 justify-start border-gray-300 bg-white text-left font-normal"
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{{ dateRangeDisplay }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-0">
|
||||
<RangeCalendar />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Filter Button -->
|
||||
<Button
|
||||
variant="outline"
|
||||
class="ml-auto border-orange-500 bg-orange-50 text-orange-600 hover:bg-orange-100"
|
||||
@click="handleFilter"
|
||||
>
|
||||
<FilterIcon class="mr-2 h-4 w-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="overflow-x-auto p-4">
|
||||
<AppChemotherapyListVerification
|
||||
:data="filtered"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Verification -->
|
||||
<DialogVerification
|
||||
v-model:open="isDialogVerificationOpen"
|
||||
:tanggal-pemeriksaan="selectedSchedule?.tanggalPemeriksaan"
|
||||
:jadwal-tanggal-pemeriksaan="selectedSchedule?.jadwalTanggalPemeriksaan"
|
||||
@submit="handleDialogSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,7 +43,7 @@ interface Props {
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
let units = ref<{ value: string; label: string }[]>([])
|
||||
const units = ref<{ value: string; label: string }[]>([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
|
||||
import { handleActionSave,} from '~/handlers/control-letter.handler'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { type ControlLetter } from '~/models/control-letter'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const controlLetterForm = ref<ExposedForm<any> | null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
|
||||
const selectedUnitId = ref<number|null>(null)
|
||||
const selectedSpecialistId = ref<number|null>(null)
|
||||
const selectedSubSpecialistId = ref<number|null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const controlLetter: ControlLetter = await composeFormData()
|
||||
let createdControlLetterId = 0
|
||||
|
||||
const response = await handleActionSave(
|
||||
controlLetter,
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
|
||||
const data = (response?.body?.data ?? null)
|
||||
if (!data) return
|
||||
createdControlLetterId = data.id
|
||||
|
||||
// // If has callback provided redirect to callback with patientData
|
||||
if (props.callbackUrl) {
|
||||
navigateTo(props.callbackUrl + '?control-letter-id=' + controlLetter.id)
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<ControlLetter> {
|
||||
const [controlLetter,] = await Promise.all([
|
||||
controlLetterForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [controlLetter]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = controlLetter?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
|
||||
provide("selectedUnitId", selectedUnitId);
|
||||
provide("selectedSpecialistId", selectedSpecialistId);
|
||||
provide("selectedSubSpecialistId", selectedSubSpecialistId);
|
||||
// #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>
|
||||
<AppControlLetterEntryForm
|
||||
ref="controlLetterForm"
|
||||
:schema="ControlLetterSchema"
|
||||
:selected-unit-id="selectedUnitId"
|
||||
:selected-specialist-id="selectedSpecialistId"
|
||||
:selected-sub-specialist-id="selectedSubSpecialistId"
|
||||
/>
|
||||
|
||||
<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,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { getDetail } from '~/services/control-letter.service'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import type { ControlLetter } from '~/models/control-letter'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
record_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
}>(), {
|
||||
redirectToForm: () => {},
|
||||
})
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
|
||||
const router = useRouter()
|
||||
const controlLetter = ref<ControlLetter | null>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Surat Kontrol',
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(props.record_id, {
|
||||
includes: "unit,specialist,subspecialist,doctor-employee-person",
|
||||
})
|
||||
if (result.success) {
|
||||
controlLetter.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':
|
||||
props.redirectToForm(props.record_id)
|
||||
break
|
||||
|
||||
case 'back':
|
||||
goBack()
|
||||
break
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
|
||||
|
||||
<AppControlLetterPreview :instance="controlLetter" @click="handleAction" />
|
||||
</template>
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { getDetail, update } from '~/services/control-letter.service'
|
||||
|
||||
import {
|
||||
// for form entry
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
} from '~/handlers/control-letter.handler'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import type { ControlLetter } from '~/models/control-letter'
|
||||
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
|
||||
import { formatDateYyyyMmDd } from '~/lib/date'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const controlLetterForm = ref<ExposedForm<any> | null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
|
||||
const isConfirmationOpen = ref(false)
|
||||
const controlLetter = ref({})
|
||||
|
||||
const selectedUnitId = ref<number|null>(null)
|
||||
const selectedSpecialistId = ref<number|null>(null)
|
||||
const selectedSubSpecialistId = ref<number|null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(props.record_id)
|
||||
if (result.success) {
|
||||
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
|
||||
selectedUnitId.value = responseData?.unit_code
|
||||
selectedSpecialistId.value = responseData?.specialist_code
|
||||
selectedSubSpecialistId.value = responseData?.subspecialist_code
|
||||
|
||||
controlLetter.value = responseData
|
||||
controlLetterForm.value?.setValues(responseData)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const response = await handleActionEdit(
|
||||
props.record_id,
|
||||
await composeFormData(),
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<ControlLetter> {
|
||||
const [controlLetter,] = await Promise.all([
|
||||
controlLetterForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [controlLetter]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = controlLetter?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
|
||||
provide("selectedUnitId", selectedUnitId);
|
||||
provide("selectedSpecialistId", selectedSpecialistId);
|
||||
provide("selectedSubSpecialistId", selectedSubSpecialistId);
|
||||
// #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>
|
||||
<AppControlLetterEntryForm
|
||||
ref="controlLetterForm"
|
||||
:schema="ControlLetterSchema"
|
||||
:selected-unit-id="selectedUnitId"
|
||||
:selected-specialist-id="selectedSpecialistId"
|
||||
:selected-sub-specialist-id="selectedSubSpecialistId"
|
||||
/>
|
||||
|
||||
<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,223 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Imports
|
||||
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 { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { getList, remove } from '~/services/control-letter.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
|
||||
// import type { PagePermission } from '~/models/role'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
import { permissions } from '~/const/page-permission/ambulatory'
|
||||
import { unauthorizedToast } from '~/lib/utils'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import HistoryDialog from '~/components/app/control-letter/history-dialog.vue'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
// #endregion
|
||||
|
||||
// #region Permission
|
||||
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
|
||||
const { getPagePermissions } = useRBAC()
|
||||
const pagePermission = getPagePermissions(roleAccess)
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
redirectToDetail?: (myRecord_id: string|number) => void
|
||||
}>(), {
|
||||
redirectToForm: () => { },
|
||||
redirectToDetail: () => { }
|
||||
})
|
||||
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
|
||||
entityName: 'control-letter',
|
||||
})
|
||||
const historyData = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'control-letter-history',
|
||||
})
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
provide('isHistoryDialogOpen', isHistoryDialogOpen)
|
||||
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
const isRequirementsMet = ref(true)
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref(new Date().toISOString())
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Surat Kontrol",
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
if (pagePermission.canCreate) {
|
||||
headerPrep.addNav = {
|
||||
label: "Surat Kontrol",
|
||||
onClick: () => {
|
||||
console.log('redirectToForm')
|
||||
console.log(props.redirectToForm())
|
||||
props.redirectToForm()
|
||||
},
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
getListData()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function getListData() {
|
||||
try {
|
||||
summaryLoading.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
console.error('Error fetching Data:', error)
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle confirmation result
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
await fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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('timestamp', isLoading)
|
||||
provide('table_data_loader', isLoading)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
props.redirectToDetail(recId.value)
|
||||
break
|
||||
|
||||
case ActionEvents.showEdit:
|
||||
if(pagePermission.canUpdate){
|
||||
props.redirectToForm(recId.value)
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
if(pagePermission.canDelete){
|
||||
isRecordConfirmationOpen.value = true
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WarningAlert v-if="!isRequirementsMet"
|
||||
class="mb-5"
|
||||
text="Syarat pembuatan surat kontrol belum terpenuhi"
|
||||
:description="[
|
||||
'Lanjutan Penatalaksanaan Pasien harus pulang/KRS.',
|
||||
'Status Resume Medis harus tervalidasi.'
|
||||
]" />
|
||||
|
||||
<div v-else>
|
||||
<Header v-model:search="searchInput"
|
||||
:prep="{ ...headerPrep }"
|
||||
:ref-search-nav="refSearchNav"
|
||||
@search="handleSearch" />
|
||||
|
||||
<div class="mb-3 flex justify-end items-center">
|
||||
<Button variant="outline"
|
||||
class="gap-1 bg-transparent items-center text-orange-400 border-orange-400"
|
||||
@click="isHistoryDialogOpen = true">
|
||||
<Icon name="i-lucide-history" class="h-4 w-4" /> Riwayat Program Nasional</Button>
|
||||
</div>
|
||||
|
||||
<AppControlLetterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||
</Dialog>
|
||||
|
||||
<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>
|
||||
|
||||
<HistoryDialog
|
||||
v-model:is-modal-open="isHistoryDialogOpen"
|
||||
:data="historyData.data.value"
|
||||
:pagination-meta="historyData.paginationMeta"
|
||||
@page-change="historyData.handlePageChange"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Detail from './detail.vue'
|
||||
import Add from './add.vue'
|
||||
import Edit from './edit.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list && !crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:redirectToForm="goToEntry"
|
||||
:redirectToDetail="goToDetail"/>
|
||||
<Detail v-else-if="crudQueryParams.mode === crudQueryParamsMode.list && crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId"
|
||||
:redirectToForm="goToEntry"/>
|
||||
<Add v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && !crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id" />
|
||||
<Edit v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
|
||||
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
|
||||
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
|
||||
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// mcu src category
|
||||
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
|
||||
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
|
||||
|
||||
// mcu src
|
||||
import { type McuSrc } from '~/models/mcu-src'
|
||||
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
|
||||
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
|
||||
|
||||
// mcu order
|
||||
import { getDetail } from '~/services/mcu-order.service'
|
||||
import Detail from '~/components/app/mcu-order/detail.vue'
|
||||
|
||||
// mcu order item, manually not using composable
|
||||
import {
|
||||
getList as getMcuOrderItemList,
|
||||
create as createMcuOrderItem,
|
||||
remove as removeMcuOrderItem,
|
||||
} from '~/services/mcu-order-item.service'
|
||||
import { type McuOrderItem } from '~/models/mcu-order-item'
|
||||
import ItemListEntry from '~/components/app/mcu-order-item/list-entry.vue'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
// declaration & flows
|
||||
|
||||
// MCU Order
|
||||
const { getQueryParam } = useQueryParam()
|
||||
const id = getQueryParam('id')
|
||||
const dataRes = await getDetail(
|
||||
typeof id === 'string' ? parseInt(id) : 0,
|
||||
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
|
||||
)
|
||||
const data = dataRes.body?.data
|
||||
|
||||
// MCU items
|
||||
const items = ref<McuOrderItem[]>([])
|
||||
|
||||
// MCU Categories
|
||||
const mcuSrcCategoryRes = await getMcuCategoryList()
|
||||
const mcuSrcCategories = mcuSrcCategoryRes.body?.data
|
||||
const selectedMcuSrcCategory_code = ref('')
|
||||
|
||||
// MCU Sources
|
||||
const mcuSrcs = ref<McuSrc[]>([])
|
||||
|
||||
// const {
|
||||
// data: items,
|
||||
// fetchData: getItems,
|
||||
// } = usePaginatedList<McuOrderItem> ({
|
||||
// fetchFn: async ({ page, search }) => {
|
||||
// const result = await getMcuOrderItemList({ 'mcu-order-id': id, search, page })
|
||||
// if (result.success) {
|
||||
// items.value = result.body.data
|
||||
// }
|
||||
// return { success: result.success || false, body: result.body || {} }
|
||||
// },
|
||||
// entityName: 'mcu-order-item',
|
||||
// })
|
||||
|
||||
const { backToList } = useQueryCRUDMode()
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail dan List Item Order Radiologi ',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
const pickerDialogOpen = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await getItems()
|
||||
})
|
||||
|
||||
watch(selectedMcuSrcCategory_code, async () => {
|
||||
const res = await getMcuSrcList({ 'mcu-src-category-code': selectedMcuSrcCategory_code.value })
|
||||
mcuSrcs.value = res.body?.data
|
||||
})
|
||||
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
function requestItem() {
|
||||
pickerDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function pickItem(item: McuSrc) {
|
||||
const exItem = items.value.find(e => e.mcuSrc_id === item.id)
|
||||
if (exItem) {
|
||||
await removeMcuOrderItem(exItem.id)
|
||||
await getItems()
|
||||
} else {
|
||||
const intId = parseInt(id?.toString() || '0')
|
||||
await createMcuOrderItem({
|
||||
mcuOrder_id: intId,
|
||||
mcuSrc_id: item.id,
|
||||
})
|
||||
await getItems()
|
||||
}
|
||||
}
|
||||
|
||||
async function getItems() {
|
||||
const itemsRes = await getMcuOrderItemList({ 'mcu-order-id': id, includes: 'mcuSrc,mcuSrc-mcuSrcCategory' })
|
||||
if (itemsRes.success) {
|
||||
items.value = itemsRes.body.data
|
||||
} else {
|
||||
items.value = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<Detail :data="data" />
|
||||
|
||||
<ItemListEntry
|
||||
:data="items"
|
||||
@requestItem="requestItem"/>
|
||||
<Separator class="my-5" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<Nav @click="navClick" />
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:open="pickerDialogOpen"
|
||||
title="Pilih Item"
|
||||
size="2xl"
|
||||
prevent-outside
|
||||
>
|
||||
<ScrCategorySwitcher :data="mcuSrcCategories" v-model="selectedMcuSrcCategory_code" />
|
||||
<McuSrcPicker v-model="items" :data-source="mcuSrcs" @pick="pickItem" />
|
||||
<Separator />
|
||||
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionSave,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/mcu-order.handler'
|
||||
|
||||
// Apps
|
||||
import { getList, getDetail } from '~/services/mcu-order.service'
|
||||
import List from '~/components/app/mcu-order/micro-list.vue'
|
||||
import type { McuOrder } from '~/models/mcu-order'
|
||||
|
||||
const route = useRoute()
|
||||
const { setQueryParams } = useQueryParam()
|
||||
|
||||
const title = ref('')
|
||||
|
||||
const plainEid = route.params.id
|
||||
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList<McuOrder>({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
search,
|
||||
page,
|
||||
'scope-code': "cp-lab",
|
||||
'encounter-id': encounter_id,
|
||||
includes: 'doctor,doctor-employee,doctor-employee-person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'mcu-order'
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Order Lab Mikro',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Detail Order Lab PK'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Edit Order Lab PK'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
watch([isFormEntryDialogOpen], async () => {
|
||||
if (isFormEntryDialogOpen.value) {
|
||||
isFormEntryDialogOpen.value = false;
|
||||
const saveResp = await handleActionSave({ encounter_id, scope_code: "cp-lab" }, getMyList, () =>{}, toast)
|
||||
if (saveResp.success) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': saveResp.body?.data?.id.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
})
|
||||
|
||||
function cancel(data: McuOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
function edit(data: McuOrder) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': data.id.toString()
|
||||
})
|
||||
recItem.value = data
|
||||
}
|
||||
|
||||
function submit(data: McuOrder) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<List
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="cancel"
|
||||
@edit="edit"
|
||||
@submit="submit"
|
||||
/>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useQueryMode } from '@/composables/useQueryMode'
|
||||
|
||||
import List from './list.vue'
|
||||
import Form from './form.vue'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const route = useRoute()
|
||||
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<List
|
||||
v-if="mode === 'list'"
|
||||
:encounter="props.encounter"
|
||||
@add="goToEntry"
|
||||
@edit="goToEntry"
|
||||
/>
|
||||
<Form
|
||||
v-else
|
||||
@back="backToList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/cprj/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { CprjSoapiSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const fungsional = ref([])
|
||||
const schema = CprjSoapiSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
typeCode: 'dev-record',
|
||||
value: '',
|
||||
})
|
||||
const model = ref({
|
||||
ppa: '',
|
||||
ppa_name: '',
|
||||
subjective: '',
|
||||
objective: '',
|
||||
assesment: '',
|
||||
plan: '',
|
||||
review: '',
|
||||
})
|
||||
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
onMounted(() => {})
|
||||
|
||||
const cprjRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await cprjRef.value?.validate()
|
||||
console.log('result', result)
|
||||
if (result?.valid) {
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{},
|
||||
toast,
|
||||
)
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="cprjRef"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="function"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/soapi/list.vue'
|
||||
import Entry from '~/components/app/cprj/entry.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/consultation.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
const route = useRoute()
|
||||
|
||||
const { recordId } = useQueryCRUDRecordId()
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
let units = ref<{ value: string; label: string }[]>([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
const id = route.params.id
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
'encounter-id': id,
|
||||
'type-code': 'dev-record',
|
||||
includes: 'encounter',
|
||||
search,
|
||||
page,
|
||||
})
|
||||
if (result.success) {
|
||||
data.value = result.body.data
|
||||
}
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'cprj',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'CPRJ',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
goToEntry()
|
||||
emits('add')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Detail Konsultasi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
emits('edit')
|
||||
recordId.value = recId.value
|
||||
console.log('recordId', recId.value)
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<List
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,254 @@
|
||||
<script setup lang="ts">
|
||||
// Composables
|
||||
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Pub components
|
||||
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
|
||||
// Refs / src
|
||||
import { type Device } from '~/models/device'
|
||||
import { getList as getDeviceList } from '~/services/device.service'
|
||||
|
||||
// Device order things
|
||||
import { getDetail, remove, submit } from '~/services/device-order.service'
|
||||
import EntryForm from '~/components/app/device-order/entry-form.vue'
|
||||
|
||||
// Items
|
||||
import {
|
||||
getList as getItemList,
|
||||
create as createItem,
|
||||
update as updateItem,
|
||||
} from '~/services/device-order-item.service'
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/device-order-item.handler'
|
||||
import { genDeviceOrderItem, type DeviceOrderItem } from '~/models/device-order-item'
|
||||
import ItemListEntry from '~/components/app/device-order-item/list-entry.vue'
|
||||
import ItemEntry from '~/components/app/device-order-item/entry-form.vue'
|
||||
|
||||
// Header
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Tambah Order Alkes',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
// Device order things
|
||||
const { getQueryParam } = useQueryParam()
|
||||
const rawId = getQueryParam('id')
|
||||
const id = typeof rawId === 'string' ? parseInt(rawId) : 0
|
||||
const dataRes = await getDetail(id, { includes: 'doctor,doctor-employee,doctor-employee-person' })
|
||||
const data = ref(dataRes.body?.data || null)
|
||||
const devices = ref<Device[]>([])
|
||||
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
const isDeleteConfirmationOpen = ref(false)
|
||||
|
||||
// Items
|
||||
const {
|
||||
data: items,
|
||||
isLoading,
|
||||
fetchData: getDeviceOrderItems,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getItemList({
|
||||
'device-order-id': id,
|
||||
includes: 'device',
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'division',
|
||||
})
|
||||
const selectedItem = ref<DeviceOrderItem>(genDeviceOrderItem())
|
||||
const isItemDetailDialogOpen = ref(false)
|
||||
const isItemEntryDialogOpen = ref(false)
|
||||
const isItemDelConfirmDialogOpen = ref(false)
|
||||
|
||||
selectedItem.value.deviceOrder_id = id
|
||||
|
||||
// Last navs
|
||||
const { backToList } = useQueryCRUDMode()
|
||||
|
||||
// Reactivities
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
let item: DeviceOrderItem | null
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
item = pickItem()
|
||||
if (item) {
|
||||
isItemDetailDialogOpen.value = true
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
item = pickItem()
|
||||
if (item) {
|
||||
isItemEntryDialogOpen.value = true
|
||||
getDevices('', item.device_code)
|
||||
}
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isItemDelConfirmDialogOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getDeviceOrderItems()
|
||||
})
|
||||
|
||||
// Functions
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
} else if (type === 'delete') {
|
||||
isDeleteConfirmationOpen.value = true
|
||||
} else if (type === 'submit') {
|
||||
isSubmitConfirmationOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOrder() {
|
||||
const res = await remove(id)
|
||||
if (res.success) {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOrder() {
|
||||
const res = await submit(id)
|
||||
if (res.success) {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
isItemEntryDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function getDevices(value: string, code?: string) {
|
||||
const res = await getDeviceList({ 'search': value, 'code': code })
|
||||
if (res.success) {
|
||||
devices.value = res.body.data
|
||||
} else {
|
||||
devices.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function pickItem(): DeviceOrderItem | null {
|
||||
const item = items.value.find(item => item.id === recId.value)
|
||||
selectedItem.value = item
|
||||
return item
|
||||
}
|
||||
|
||||
async function saveItem() {
|
||||
let res: any;
|
||||
if(!selectedItem.value.id) {
|
||||
res = await createItem(selectedItem.value)
|
||||
} else {
|
||||
res = await updateItem(selectedItem.value.id, selectedItem.value)
|
||||
}
|
||||
if (res.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
getDeviceOrderItems()
|
||||
isItemEntryDialogOpen.value = false
|
||||
selectedItem.value = genDeviceOrderItem()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<EntryForm :data="data" @add="addItem" />
|
||||
|
||||
<ItemListEntry :data="items" @add="addItem" />
|
||||
|
||||
<Separator class="my-5" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<Nav @click="navClick" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm delete -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isDeleteConfirmationOpen"
|
||||
action="delete"
|
||||
:record="data"
|
||||
@confirm="removeOrder()"
|
||||
/>
|
||||
|
||||
<!-- Confirm submit -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
customTitle="Ajukan Order"
|
||||
customMessage="Akan dilakukan pengajuan order alat kesehatan"
|
||||
customConfirmText="Ajukan"
|
||||
:record="data"
|
||||
@confirm="submitOrder"
|
||||
@cancel=""
|
||||
/>
|
||||
|
||||
<!-- Item entry -->
|
||||
<Dialog
|
||||
v-model:open="isItemEntryDialogOpen"
|
||||
title="Item Order"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<ItemEntry
|
||||
:data="selectedItem"
|
||||
:devices="devices"
|
||||
@close="isItemEntryDialogOpen = false"
|
||||
@save="saveItem"
|
||||
@update:searchText="getDevices"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isItemDelConfirmDialogOpen"
|
||||
action="delete"
|
||||
size="md"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getDeviceOrderItems, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<DE.Block mode="preview" label-size="small">
|
||||
<DE.Cell>
|
||||
<DE.Label>Nama</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.device.name }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label>Dosis</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.quantity }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</DE.Block>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/device-order/list.vue'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Device order things
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
handleActionSave,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/device-order.handler'
|
||||
import { getList, submit } from '~/services/device-order.service'
|
||||
import type { ToastFn } from '~/handlers/_handler'
|
||||
import { type DeviceOrder } from "~/models/device-order";
|
||||
import ConfirmationInfo from '~/components/app/device-order/confirmation-info.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const encounter_id = props.encounter_id
|
||||
|
||||
// Header
|
||||
const voidFn = () => {}
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Order Alkes',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: async () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isReadonly.value = false
|
||||
const saveResp = await handleActionSave({ encounter_id }, voidFn, voidFn, voidFn)
|
||||
if (saveResp.success) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': saveResp.body?.data?.id.toString()
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// List
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
'encounter-id': encounter_id,
|
||||
search: params.search,
|
||||
includes: 'doctor,doctor-employee,doctor-employee-person,items,items-device',
|
||||
page: params.page,
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'device-order',
|
||||
})
|
||||
|
||||
// Selected item
|
||||
const selectedItem = ref<DeviceOrder | null>()
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
const isDeleteConfirmationOpen = ref(false)
|
||||
const { setQueryParams } = useQueryParam()
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
let item: DeviceOrder | null
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': recId.value.toString()
|
||||
})
|
||||
break
|
||||
case ActionEvents.showConfirmSubmit:
|
||||
selectedItem.value = pickItem()
|
||||
isSubmitConfirmationOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
selectedItem.value = pickItem()
|
||||
isDeleteConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
recAction.value = '';
|
||||
})
|
||||
|
||||
async function handleActionSubmit(id: number, refresh: () => void, toast: ToastFn) {
|
||||
const result = await submit(id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
setTimeout(refresh, 300)
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: 'Gagal menjalankan perintah', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
function pickItem(): DeviceOrder | null {
|
||||
const item = data.value.find(item => item.id === recId.value)
|
||||
selectedItem.value = item
|
||||
return item
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<List
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
<!--
|
||||
@cancel="cancel"
|
||||
@edit="edit"
|
||||
@submit="submit"
|
||||
-->
|
||||
|
||||
<!-- Submit Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
customTitle="Ajukan Order"
|
||||
customMessage="Akan dilakukan pengajuan order alat kesehatan"
|
||||
customConfirmText="Ajukan"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionSubmit(recId, getMyList, toast)"
|
||||
>
|
||||
<ConfirmationInfo :data="selectedItem" />
|
||||
</RecordConfirmation>
|
||||
|
||||
<!-- Del Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isDeleteConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
>
|
||||
<ConfirmationInfo :data="selectedItem" />
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -54,6 +54,7 @@ const {
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'division,Employee.Person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
@@ -61,7 +62,7 @@ const {
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Divisi',
|
||||
title: 'Divisi - Posisi',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
@@ -104,13 +105,13 @@ const getCurrentDivisionDetail = async (id: number | string) => {
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentDivisionDetail(recId.value)
|
||||
title.value = 'Detail Divisi'
|
||||
getCurrentDivisionDetail(recItem.value.code)
|
||||
title.value = 'Detail Divisi Position'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentDivisionDetail(recId.value)
|
||||
title.value = 'Edit Divisi'
|
||||
getCurrentDivisionDetail(recItem.value.code)
|
||||
title.value = 'Edit Divisi Position'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
@@ -120,9 +121,19 @@ watch([recId, recAction], () => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
divisions.value = await getDivisionLabelList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
await getDivisionList()
|
||||
try {
|
||||
divisions.value = await getDivisionLabelList({ sort: 'createdAt:asc', 'page-size': 100 }, true)
|
||||
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
await getDivisionList()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -142,7 +153,7 @@ onMounted(async () => {
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Divisi'"
|
||||
:title="!!recItem ? title : 'Tambah Divisi Position'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
@@ -161,9 +172,8 @@ onMounted(async () => {
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: DivisionPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getDivisionList, resetForm, toast)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getDivisionList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getDivisionList, resetForm, toast)
|
||||
@@ -178,7 +188,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getDivisionList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getDivisionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
// Service
|
||||
import type { Division } from '~/models/division'
|
||||
import { getDetail as getDetailDivision } from '~/services/division.service'
|
||||
|
||||
// #region division positions
|
||||
import { config } from '~/components/app/division/detail/list-cfg'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// Types
|
||||
import { DivisionPositionSchema, type DivisionPositionFormData } from '~/schemas/division-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/division-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail as getDetailDivisionPosition } from '~/services/division-position.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
|
||||
const title = ref('')
|
||||
// #endregion
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
divisionId: string
|
||||
}>()
|
||||
const division = ref<Division>({} as Division)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getDivisionPositionList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
'division-id': props.divisionId,
|
||||
includes: 'Employee.Person',
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
'page-no-limit': true,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'division-position',
|
||||
})
|
||||
|
||||
const dataMap = computed(() => {
|
||||
return data.value.map((v, i) => {
|
||||
return {
|
||||
...v,
|
||||
index: i + 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Divisi',
|
||||
icon: 'i-lucide-user',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah Jabatan',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await getDetailDivision(props.divisionId)
|
||||
if (result.success) {
|
||||
division.value = result.body.data || {}
|
||||
}
|
||||
|
||||
const res = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
employees.value = res
|
||||
} catch (err) {
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
console.log(recId, recAction)
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
getDetailDivisionPosition(recItem.value.code)
|
||||
title.value = 'Edit Jabatan'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
|
||||
<AppDivisionDetail :division="division" />
|
||||
<div class="h-6"></div>
|
||||
|
||||
<LazyAppDivisionDetailList
|
||||
:data="dataMap"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Jabatan'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppDivisionPositionEntry
|
||||
:schema="DivisionPositionSchema"
|
||||
:division-id="divisionId"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: DivisionPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getDivisionPositionList, onResetState, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getDivisionPositionList, onResetState, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getDivisionPositionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@ import { toast } from '~/components/pub/ui/toast'
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { DivisionSchema, type DivisionFormData } from '~/schemas/division.schema'
|
||||
import type { Division } from "~/models/division"
|
||||
import type { Division } from '~/models/division'
|
||||
import type { TreeItem } from '~/models/_base'
|
||||
|
||||
// Handlers
|
||||
@@ -78,6 +78,7 @@ const headerPrep: HeaderPrep = {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
@@ -104,12 +105,23 @@ const getCurrentDivisionDetail = async (id: number | string) => {
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentDivisionDetail(recId.value)
|
||||
title.value = 'Detail Divisi'
|
||||
isReadonly.value = true
|
||||
if (recItem.value.code.length > 0) {
|
||||
navigateTo({
|
||||
name: 'org-src-division-id',
|
||||
params: {
|
||||
id: recItem.value.code,
|
||||
},
|
||||
})
|
||||
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = false
|
||||
isReadonly.value = false
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentDivisionDetail(recId.value)
|
||||
getCurrentDivisionDetail(recItem.value.code)
|
||||
title.value = 'Edit Divisi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
@@ -131,7 +143,7 @@ watch(
|
||||
if (result.success) {
|
||||
const currentData = result.body.data || []
|
||||
const normalizedData = currentData.filter((division: Division) => !division.parent_id)
|
||||
divisionsTrees.value = getValueTreeItems(normalizedData)
|
||||
divisionsTrees.value = getValueTreeItems(normalizedData, true)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -175,9 +187,9 @@ onMounted(async () => {
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: DivisionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getDivisionList, resetForm, toast)
|
||||
// console.log(values)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getDivisionList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getDivisionList, resetForm, toast)
|
||||
@@ -192,7 +204,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getDivisionList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getDivisionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||
import { toFormData } from '~/lib/utils'
|
||||
import { uploadAttachment } from '~/services/supporting-document.service'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
const { user } = useUserStore()
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
const initialValues = {
|
||||
officer: user.user_name,
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const inputData = await composeFormData()
|
||||
const inputFormData: FormData = toFormData(inputData)
|
||||
|
||||
const response = await handleActionSave(inputFormData, () => { }, () => { }, toast, )
|
||||
// const data = (response?.body?.data ?? null)
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
inputForm.value?.setValues({
|
||||
...inputForm.value?.values,
|
||||
ref_id: props.encounter_id,
|
||||
upload_employee_id: user.employee_id
|
||||
})
|
||||
|
||||
const [inputFormState,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [inputFormState]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) {
|
||||
toast({ title: 'Form validation failed', variant: 'destructive',})
|
||||
return Promise.reject('Form validation failed')
|
||||
}
|
||||
const formData = inputFormState?.values
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') goBack()
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #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">
|
||||
<h1>Upload Dokumen</h1>
|
||||
</div>
|
||||
<AppDocumentUploadEntryForm
|
||||
ref="inputForm"
|
||||
:schema="DocumentUploadSchema"
|
||||
:initial-values="initialValues"
|
||||
/>
|
||||
|
||||
<div class="mb-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,127 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { handleActionSave,} from '~/handlers/supporting-document.handler'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { DocumentUploadSchema } from '~/schemas/document-upload.schema'
|
||||
import { getDetail } from '~/services/supporting-document.service'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const docId = props.record_id
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
const { user } = useUserStore()
|
||||
const initialValues = {
|
||||
officer: user.user_name,
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(docId)
|
||||
if (result.success) {
|
||||
inputForm.value?.setValues(result.body.data)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const inputData = await composeFormData()
|
||||
let createdDataId = 0
|
||||
|
||||
// const response = await handleActionSave(
|
||||
// inputData,
|
||||
// () => { },
|
||||
// () => { },
|
||||
// toast,
|
||||
// )
|
||||
|
||||
// const data = (response?.body?.data ?? null)
|
||||
// if (!data) return
|
||||
// createdDataId = data.id
|
||||
|
||||
// // // If has callback provided redirect to callback with patientData
|
||||
// if (props.callbackUrl) {
|
||||
// navigateTo(props.callbackUrl + '?control-letter-id=' + inputData.id)
|
||||
// }
|
||||
// goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
const [inputFormState,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [inputFormState]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = inputFormState?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') goBack()
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #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">
|
||||
<h1>Update Upload Dokumen</h1>
|
||||
</div>
|
||||
<AppDocumentUploadEntryForm
|
||||
ref="inputForm"
|
||||
:schema="DocumentUploadSchema"
|
||||
:initial-values="initialValues"
|
||||
/>
|
||||
|
||||
<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,168 @@
|
||||
<script setup lang="ts">
|
||||
// #region Imports
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
import type { HeaderPrep, RefSearchNav, } from '~/components/pub/my-ui/data/types'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { getList, remove } from '~/services/supporting-document.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import type { Permission, RoleAccesses, } from '~/models/role'
|
||||
import { unauthorizedToast } from '~/lib/utils'
|
||||
import { permissions } from '~/const/page-permission/ambulatory'
|
||||
import { usePageChecker } from '~/lib/page-checker'
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region Permission
|
||||
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
|
||||
const { getPagePermissions } = useRBAC()
|
||||
const pagePermission = getPagePermissions(roleAccess)
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
}>(), {
|
||||
redirectToForm: () => { }
|
||||
})
|
||||
|
||||
const { data, paginationMeta, handlePageChange, handleSearch, searchInput, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({
|
||||
'encounter-id': props.encounter_id,
|
||||
// includes: "employee",
|
||||
...params,
|
||||
}),
|
||||
entityName: 'encounter-document',
|
||||
})
|
||||
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref<number>(0)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Upload Dokumen",
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
if (pagePermission.canCreate) {
|
||||
headerPrep.addNav = {
|
||||
label: "Upload Dokumen",
|
||||
onClick: () => {
|
||||
props.redirectToForm()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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('timestamp', timestamp)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
isDocPreviewDialogOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showEdit:
|
||||
if(pagePermission.canUpdate){
|
||||
props.redirectToForm(recId.value)
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
if(pagePermission.canDelete){
|
||||
isRecordConfirmationOpen.value = true
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }"
|
||||
v-model:search="searchInput"
|
||||
:ref-search-nav="refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<AppDocumentUploadList :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?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record?.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<DocPreviewDialog :link="recItem?.filePath" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Add from './add.vue'
|
||||
import Edit from './edit.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list" :encounter_id="encounter_id" :redirectToForm="goToEntry" />
|
||||
<Add v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && !crudQueryParams.recordId" :encounter_id="encounter_id" />
|
||||
<Edit v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry && crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" />
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>halo</div>
|
||||
</template>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import AssesmentFunctionList from '~/components/app/encounter/assesment-function/list.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (_val: string) => {
|
||||
// filter patient list
|
||||
},
|
||||
onClear: () => {
|
||||
// clear url param
|
||||
},
|
||||
}
|
||||
|
||||
// Loading state management
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
title: props.label,
|
||||
icon: 'i-lucide-users',
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => navigateTo('/rehab/registration-queue/sep-prosedur/add'),
|
||||
},
|
||||
}
|
||||
|
||||
async function getPatientList() {
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
})
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
|
||||
<AssesmentFunctionList :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
// Components
|
||||
import EncounterPatientInfo from '~/components/app/encounter/patient-info.vue'
|
||||
|
||||
// Models
|
||||
import { genEncounter } from '~/models/encounter'
|
||||
|
||||
// Handlers
|
||||
import { getEncounterData } from '~/handlers/encounter-process.handler'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const data = ref<any>(genEncounter())
|
||||
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
|
||||
|
||||
async function getData() {
|
||||
data.value = await getEncounterData(id)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getData()
|
||||
})
|
||||
|
||||
function handleClick(type: string) {
|
||||
if (type === 'draft') {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-4">
|
||||
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" @click="handleClick" />
|
||||
</div>
|
||||
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,51 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// Components
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import AppEncounterEntryForm from '~/components/app/encounter/entry-form.vue'
|
||||
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||
import AppViewHistory from '~/components/app/sep/view-history.vue'
|
||||
|
||||
// Helpers
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
// Handlers
|
||||
import { useEncounterEntry } from '~/handlers/encounter-entry.handler'
|
||||
import { useIntegrationSepEntry } from '~/handlers/integration-sep-entry.handler'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
id: number
|
||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient' | 'outpatient'
|
||||
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
||||
formType: string
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
const route = useRoute()
|
||||
const formRef = ref<InstanceType<typeof AppEncounterEntryForm> | null>(null)
|
||||
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
const {
|
||||
paymentsList,
|
||||
sepNumber,
|
||||
sepsList,
|
||||
participantGroupsList,
|
||||
specialistsTree,
|
||||
doctorsList,
|
||||
recSelectId,
|
||||
isLoadingDetail,
|
||||
formObjects,
|
||||
openPatient,
|
||||
isMemberValid,
|
||||
isSepValid,
|
||||
isCheckingSep,
|
||||
isSaveDisabled,
|
||||
isLoading,
|
||||
patients,
|
||||
selectedDoctor,
|
||||
selectedPatient,
|
||||
selectedPatientObject,
|
||||
paginationMeta,
|
||||
toNavigateSep,
|
||||
getListPath,
|
||||
handleInit,
|
||||
getFetchEncounterDetail,
|
||||
handleSaveEncounter,
|
||||
getPatientsList,
|
||||
getPatientCurrent,
|
||||
getPatientByIdentifierSearch,
|
||||
// getIsSubspecialist,
|
||||
getDoctorInfo,
|
||||
getValidateMember,
|
||||
getValidateSepNumber,
|
||||
handleFetchDoctors,
|
||||
} = useEncounterEntry(props)
|
||||
const { recSepId, openHistory, histories, getMonitoringHistoryMappers } = useIntegrationSepEntry()
|
||||
|
||||
const debouncedSepNumber = refDebounced(sepNumber, 500)
|
||||
|
||||
///// Functions
|
||||
function handleSavePatient() {
|
||||
selectedPatientObject.value = null
|
||||
setTimeout(() => {
|
||||
getPatientCurrent(selectedPatient.value)
|
||||
}, 150)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
})
|
||||
function handleSaveClick() {
|
||||
if (formRef.value && typeof formRef.value.submitForm === 'function') {
|
||||
formRef.value.submitForm()
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(e: 'search' | 'add') {
|
||||
console.log('click', e)
|
||||
if (e === 'search') {
|
||||
isOpen.value = true
|
||||
} else if (e === 'add') {
|
||||
function handleFetch(value?: any) {
|
||||
if (value?.subSpecialistId) {
|
||||
handleFetchDoctors(value.subSpecialistId)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEvent(menu: string, value?: any) {
|
||||
if (menu === 'search') {
|
||||
getPatientsList({ 'page-size': 10, includes: 'person' }).then(() => {
|
||||
openPatient.value = true
|
||||
})
|
||||
} else if (menu === 'add') {
|
||||
navigateTo('/client/patient/add')
|
||||
} else if (menu === 'add-sep') {
|
||||
if (isSepValid.value) {
|
||||
return
|
||||
}
|
||||
toNavigateSep({
|
||||
isService: 'false',
|
||||
encounterId: props.id || null,
|
||||
sourcePath: route.path,
|
||||
resource: `${props.classCode}-${props.subClassCode}`,
|
||||
...value,
|
||||
})
|
||||
} else if (menu === 'sep-number-changed') {
|
||||
const sepNumberText = String(value || '').trim()
|
||||
if (sepNumberText.length > 5) {
|
||||
await getValidateSepNumber(sepNumberText)
|
||||
}
|
||||
} else if (menu === 'member-changed') {
|
||||
const memberText = String(value || '').trim()
|
||||
if (memberText.length > 5) {
|
||||
await getValidateMember(memberText)
|
||||
}
|
||||
} else if (menu === 'search-sep') {
|
||||
const memberText = String(value?.cardNumber || '').trim()
|
||||
if (memberText.length < 5) return
|
||||
getMonitoringHistoryMappers(memberText).then(() => {
|
||||
openHistory.value = true
|
||||
})
|
||||
return
|
||||
} else if (menu === 'save') {
|
||||
await handleSaveEncounter(value)
|
||||
} else if (menu === 'cancel') {
|
||||
navigateTo(getListPath())
|
||||
}
|
||||
}
|
||||
|
||||
provide('rec_select_id', recSelectId)
|
||||
provide('rec_sep_id', recSepId)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch(debouncedSepNumber, async (newValue) => {
|
||||
await getValidateSepNumber(newValue)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => formObjects.value?.paymentType,
|
||||
(newValue) => {
|
||||
isSepValid.value = false
|
||||
if (newValue !== 'jkn') {
|
||||
sepNumber.value = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
async (newId) => {
|
||||
if (props.formType === 'edit' && newId > 0) {
|
||||
await getFetchEncounterDetail()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await handleInit()
|
||||
if (props.formType === 'edit' && props.id > 0) {
|
||||
await getFetchEncounterDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-user" class="me-2" />
|
||||
<span class="font-semibold">{{ props.formType }}</span> Kunjungan
|
||||
<Icon
|
||||
name="i-lucide-user"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="font-semibold">{{ props.formType === 'add' ? 'Tambah' : 'Ubah' }}</span>
|
||||
Kunjungan
|
||||
</div>
|
||||
<AppEncounterEntryForm @click="onClick" />
|
||||
<AppSepSmallEntry v-if="props.id" />
|
||||
|
||||
<Dialog v-model:open="isOpen" title="Cari Pasien" size="xl" prevent-outside>
|
||||
<AppPatientPicker :data="data" />
|
||||
</Dialog>
|
||||
<AppEncounterEntryForm
|
||||
ref="formRef"
|
||||
:mode="props.formType"
|
||||
:is-loading="isLoadingDetail"
|
||||
:is-member-valid="isMemberValid"
|
||||
:is-sep-valid="isSepValid"
|
||||
:is-checking-sep="isCheckingSep"
|
||||
:payments="paymentsList"
|
||||
:seps="sepsList"
|
||||
:participant-groups="participantGroupsList"
|
||||
:specialists="specialistsTree"
|
||||
:doctorItems="doctorsList"
|
||||
:selectedDoctor="selectedDoctor"
|
||||
:patient="selectedPatientObject"
|
||||
:objects="formObjects"
|
||||
@on-select-doctor="getDoctorInfo"
|
||||
@event="handleEvent"
|
||||
@fetch="handleFetch"
|
||||
/>
|
||||
<AppViewPatient
|
||||
v-model:open="openPatient"
|
||||
v-model:selected="selectedPatient"
|
||||
:patients="patients"
|
||||
:pagination-meta="paginationMeta"
|
||||
@fetch="
|
||||
(value) => {
|
||||
if (value.search && value.search.length >= 3) {
|
||||
// Use identifier search for specific searches (NIK, RM, etc.)
|
||||
getPatientByIdentifierSearch(value.search)
|
||||
} else {
|
||||
// Use regular search for general searches
|
||||
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
|
||||
}
|
||||
}
|
||||
"
|
||||
@save="handleSavePatient"
|
||||
/>
|
||||
<AppViewHistory
|
||||
v-model:open="openHistory"
|
||||
:is-action="true"
|
||||
:histories="histories"
|
||||
/>
|
||||
<!-- Footer Actions -->
|
||||
<div class="mt-6 flex justify-end gap-2 border-t border-t-slate-300 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
class="h-[40px] min-w-[120px] rounded-md border-orange-400 text-orange-400 hover:bg-green-50 hover:text-orange-400"
|
||||
@click="handleEvent('cancel')"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-x"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
class="h-[40px] min-w-[120px] text-white"
|
||||
:disabled="isSaveDisabled"
|
||||
@click="handleSaveClick"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-save"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Simpan
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
///// Imports
|
||||
// Pub components
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
|
||||
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import * as CH from '~/components/pub/my-ui/content-header'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
|
||||
|
||||
// App libs
|
||||
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
|
||||
|
||||
// Services
|
||||
import {
|
||||
getList as getEncounterList,
|
||||
remove as removeEncounter,
|
||||
cancel as cancelEncounter,
|
||||
} from '~/services/encounter.service'
|
||||
|
||||
// Apps
|
||||
import Content from '~/components/app/encounter/list.vue'
|
||||
import FilterNav from '~/components/app/encounter/filter-nav.vue'
|
||||
import FilterForm from '~/components/app/encounter/filter-form.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
type: string
|
||||
classCode?: 'ambulatory' | 'emergency' | 'inpatient'
|
||||
subClassCode?: 'reg' | 'rehab' | 'chemo' | 'emg' | 'eon' | 'op' | 'icu' | 'hcu' | 'vk'
|
||||
canCreate?: boolean
|
||||
canUpdate?: boolean
|
||||
canDelete?: boolean
|
||||
}>()
|
||||
|
||||
///// Declarations and Flows
|
||||
// Sidebar automation
|
||||
const { setOpen } = useSidebar()
|
||||
setOpen(true)
|
||||
|
||||
// Role reactivities
|
||||
const { getActiveRole } = useUserStore()
|
||||
|
||||
// Main data
|
||||
const data = ref([])
|
||||
const dataFiltered = ref([])
|
||||
const filterParams = ref<any>({})
|
||||
const activeServicePosition = ref(getServicePosition(getActiveRole()))
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
summary: false,
|
||||
isTableLoading: false,
|
||||
@@ -20,89 +53,291 @@ const isLoading = reactive<DataTableLoader>({
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const isFormEntryDialogOpen = ref(false)
|
||||
const isFilterFormDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const isRecordCancelOpen = ref(false)
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
// Headers
|
||||
const hreaderPrep: CH.Config = {
|
||||
title: 'Kunjungan',
|
||||
icon: 'i-lucide-users',
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => navigateTo('/rehab/encounter/add'),
|
||||
onClick: () => {
|
||||
navigateTo(`/${props.classCode}/encounter/add`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
isFormEntryDialogOpen.value = true
|
||||
console.log(' 1open filter modal')
|
||||
},
|
||||
onInput: (_val: string) => {
|
||||
// filter patient list
|
||||
},
|
||||
onClear: () => {
|
||||
// clear url param
|
||||
},
|
||||
if (!props.canCreate) {
|
||||
delete hreaderPrep.addNav
|
||||
}
|
||||
|
||||
// Loading state management
|
||||
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/encounter?includes=patient,patient-person')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
// Filters
|
||||
const filter = ref<{
|
||||
installation: {
|
||||
msg: {
|
||||
placeholder: string
|
||||
}
|
||||
items: {
|
||||
value: string
|
||||
label: string
|
||||
code: string
|
||||
}[]
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
schema: any
|
||||
initialValues?: Partial<any>
|
||||
errors?: any
|
||||
}>({
|
||||
installation: {
|
||||
msg: {
|
||||
placeholder: 'Pilih',
|
||||
},
|
||||
items: [],
|
||||
},
|
||||
schema: {},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
// Recrod reactivities
|
||||
provide('activeServicePosition', activeServicePosition)
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch(getActiveRole, (role?: string) => {
|
||||
activeServicePosition.value = getServicePosition(role)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => recAction.value,
|
||||
() => {
|
||||
console.log('recAction.value', recAction.value)
|
||||
if (props.type === 'encounter') {
|
||||
if (recAction.value === 'showDetail') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/detail`)
|
||||
} else if (recAction.value === 'showEdit') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/edit`)
|
||||
} else if (recAction.value === 'showProcess') {
|
||||
navigateTo(`/rehab/encounter/${recId.value}/process`)
|
||||
} else {
|
||||
// handle other actions
|
||||
}
|
||||
} else if (props.type === 'registration') {
|
||||
if (recAction.value === 'showDetail') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/detail`)
|
||||
} else if (recAction.value === 'showEdit') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/edit`)
|
||||
} else if (recAction.value === 'showProcess') {
|
||||
navigateTo(`/rehab/registration/${recId.value}/process`)
|
||||
} else {
|
||||
// handle other actions
|
||||
}
|
||||
const basePath = `/${props.classCode}/encounter`
|
||||
if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||
isRecordConfirmationOpen.value = true
|
||||
} else if (recAction.value === ActionEvents.showCancel) {
|
||||
isRecordCancelOpen.value = true
|
||||
} else if (recAction.value === ActionEvents.showDetail) {
|
||||
navigateTo(`${basePath}/${recId.value}/detail`)
|
||||
} else if (recAction.value === ActionEvents.showEdit) {
|
||||
navigateTo(`${basePath}/${recId.value}/edit`)
|
||||
} else if (recAction.value === ActionEvents.showProcess) {
|
||||
navigateTo(`${basePath}/${recId.value}/process`)
|
||||
} else if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
recAction.value = '' // reset
|
||||
},
|
||||
)
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
})
|
||||
|
||||
/////// Functions
|
||||
async function getPatientList() {
|
||||
isLoading.isTableLoading = true
|
||||
const includesParams =
|
||||
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,Responsible_Doctor,Responsible_Doctor-employee,Responsible_Doctor-employee-person'
|
||||
data.value = []
|
||||
try {
|
||||
const params: any = { includes: includesParams, ...filterParams.value }
|
||||
if (props.classCode) {
|
||||
params.class_code = props.classCode
|
||||
}
|
||||
if (props.subClassCode) {
|
||||
params.sub_class_code = props.subClassCode
|
||||
}
|
||||
const result = await getEncounterList(params)
|
||||
if (result.success) {
|
||||
data.value = result.body?.data || []
|
||||
dataFiltered.value = [...data.value]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching encounter list:', error)
|
||||
} finally {
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilterApply(filters: { personName: string; startDate: string; endDate: string }) {
|
||||
filterParams.value = {
|
||||
'person-name': filters.personName,
|
||||
'start-date': filters.startDate,
|
||||
'end-date': filters.endDate,
|
||||
}
|
||||
getPatientList()
|
||||
}
|
||||
|
||||
// Handle confirmation result
|
||||
async function handleConfirmCancel(record: any, action: string) {
|
||||
if (action === 'deactivate' && record?.id) {
|
||||
try {
|
||||
const result = await cancelEncounter(record.id)
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: 'Berhasil',
|
||||
description: 'Kunjungan berhasil dibatalkan',
|
||||
variant: 'default',
|
||||
})
|
||||
await getPatientList() // Refresh list
|
||||
} else {
|
||||
const errorMessage = result.body?.message || 'Gagal membatalkan kunjungan'
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error cancellation encounter:', error)
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: error?.message || 'Gagal membatalkan kunjungan',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
// Reset state
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordCancelOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle confirmation result
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await removeEncounter(record.id)
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: 'Berhasil',
|
||||
description: 'Kunjungan berhasil dihapus',
|
||||
variant: 'default',
|
||||
})
|
||||
await getPatientList() // Refresh list
|
||||
} else {
|
||||
const errorMessage = result.body?.message || 'Gagal menghapus kunjungan'
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting encounter:', error)
|
||||
toast({
|
||||
title: 'Gagal',
|
||||
description: error?.message || 'Gagal menghapus kunjungan',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
// Reset state
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordConfirmationOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordCancelOpen.value = false
|
||||
}
|
||||
|
||||
function handleRemoveConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
isRecordConfirmationOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||
<CH.ContentHeader v-bind="hreaderPrep">
|
||||
<FilterNav
|
||||
:active-positon="activeServicePosition"
|
||||
@apply="handleFilterApply"
|
||||
@onExportPdf="() => {}"
|
||||
@onExportExcel="() => {}"
|
||||
@nExportCsv="() => {}"
|
||||
/>
|
||||
</CH.ContentHeader>
|
||||
|
||||
<Filter :ref-search-nav="refSearchNav" />
|
||||
<Content :data="data" />
|
||||
|
||||
<AppEncounterList :data="data" />
|
||||
|
||||
<Dialog v-model:open="isFormEntryDialogOpen" title="Filter" size="lg" prevent-outside>
|
||||
<AppEncounterFilter />
|
||||
<!-- Filter -->
|
||||
<Dialog
|
||||
v-model:open="isFilterFormDialogOpen"
|
||||
title="Filter"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<FilterForm v-bind="filter" />
|
||||
</Dialog>
|
||||
|
||||
<!-- Batal -->
|
||||
<RecordConfirmation
|
||||
v-if="canDelete"
|
||||
v-model:open="isRecordCancelOpen"
|
||||
custom-title="Batalkan Kunjungan"
|
||||
custom-message="Apakah anda yakin ingin membatalkan kunjungan pasien berikut?"
|
||||
action="deactivate"
|
||||
:record="recItem"
|
||||
@confirm="handleConfirmCancel"
|
||||
@cancel="handleCancelConfirmation"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p v-if="record?.patient?.person?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.patient.person.name }}
|
||||
</p>
|
||||
<p v-if="record?.medical_record_number">
|
||||
<strong>No RM:</strong>
|
||||
{{ record.medical_record_number }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
|
||||
<!-- Hapus -->
|
||||
<RecordConfirmation
|
||||
v-if="canDelete"
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="handleConfirmDelete"
|
||||
@cancel="handleRemoveConfirmation"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.patient?.person?.name">
|
||||
<strong>Pasien:</strong>
|
||||
{{ record.patient.person.name }}
|
||||
</p>
|
||||
<p v-if="record?.class_code">
|
||||
<strong>Kelas:</strong>
|
||||
{{ record.class_code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
<Dialog
|
||||
title="Hapus data"
|
||||
size="md"
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
>
|
||||
Hak akses tidak memenuhi kriteria untuk proses ini.
|
||||
</Dialog>
|
||||
<!-- <Pagination :pagination-meta="paginationMeta" @page-change="handlePageChange" /> -->
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { getDetail } from '~/services/encounter.service'
|
||||
|
||||
//
|
||||
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
||||
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
||||
|
||||
import { genEncounter } from '~/models/encounter'
|
||||
|
||||
// PLASE ORDER BY TAB POSITION
|
||||
import Status from '~/components/content/encounter/status.vue'
|
||||
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
||||
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
|
||||
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
||||
import DeviceOrder from '~/components/content/device-order/main.vue'
|
||||
import Prescription from '~/components/content/prescription/main.vue'
|
||||
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
||||
import Radiology from '~/components/content/radiology-order/main.vue'
|
||||
import Consultation from '~/components/content/consultation/list.vue'
|
||||
import Cprj from '~/components/content/cprj/entry.vue'
|
||||
import DocUploadList from '~/components/content/document-upload/list.vue'
|
||||
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
||||
import ResumeList from '~/components/content/resume/list.vue'
|
||||
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
||||
import VaccineDataList from '~/components/content/vaccine-data/list.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// activeTab selalu sinkron dengan query param
|
||||
const activeTab = computed({
|
||||
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
|
||||
set: (val: string) => {
|
||||
router.replace({ path: route.path, query: { tab: val } })
|
||||
},
|
||||
})
|
||||
|
||||
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const data = ref(genEncounter())
|
||||
|
||||
async function fetchDetail() {
|
||||
const res = await getDetail(id, {
|
||||
includes:
|
||||
'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person,EncounterDocuments',
|
||||
})
|
||||
if (res.body?.data) data.value = res.body?.data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||
{
|
||||
value: 'early-medical-assessment',
|
||||
label: 'Pengkajian Awal Medis',
|
||||
component: EarlyMedicalAssesmentList,
|
||||
props: { encounter: data, type: 'early-medic', label: 'Pengkajian Awal Medis' },
|
||||
},
|
||||
{
|
||||
value: 'rehab-medical-assessment',
|
||||
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
|
||||
component: EarlyMedicalRehabList,
|
||||
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
|
||||
},
|
||||
{
|
||||
value: 'function-assessment',
|
||||
label: 'Asesmen Fungsi',
|
||||
component: AssesmentFunctionList,
|
||||
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
|
||||
},
|
||||
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
|
||||
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
||||
{ value: 'patient-note', label: 'CPRJ', component: Cprj, props: { encounter: data } },
|
||||
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
||||
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
|
||||
{ value: 'device', label: 'Order Alkes' },
|
||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
|
||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
|
||||
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
|
||||
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
||||
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
||||
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
||||
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
||||
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
|
||||
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
||||
{ value: 'screening', label: 'Skrinning MPP' },
|
||||
{
|
||||
value: 'supporting-document',
|
||||
label: 'Upload Dokumen Pendukung',
|
||||
component: DocUploadList,
|
||||
props: { encounter: data },
|
||||
},
|
||||
{ value: 'vaccine-data', label: 'Data Vaksin', component: VaccineDataList, props: { encounter: data } },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-4">
|
||||
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
|
||||
</div>
|
||||
<AppEncounterQuickInfo :data="data" />
|
||||
<CompTab
|
||||
:data="tabs"
|
||||
:initial-active-tab="activeTab"
|
||||
@change-tab="activeTab = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,79 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
///// Imports
|
||||
// Pubs components
|
||||
import ContentSwitcher from '~/components/pub/my-ui/content-switcher/content-switcher.vue'
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar/utils'
|
||||
import SubMenu from '~/components/pub/my-ui/menus/submenu.vue'
|
||||
import ContentNavBa from '~/components/pub/my-ui/nav-content/ba.vue'
|
||||
|
||||
import { getDetail } from '~/services/encounter.service'
|
||||
// App libs
|
||||
import { getServicePosition } from '~/lib/roles' // previously getPositionAs
|
||||
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
// App Models
|
||||
import { genEncounter, type Encounter } from '~/models/encounter'
|
||||
|
||||
// Components
|
||||
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
|
||||
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
|
||||
// Handlers
|
||||
import type { EncounterProps } from '~/handlers/encounter-init.handler'
|
||||
import { getEncounterData } from '~/handlers/encounter-process.handler'
|
||||
import { getMenuItems } from '~/handlers/encounter-init.handler'
|
||||
|
||||
import Status from '~/components/app/encounter/status.vue'
|
||||
import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
|
||||
// PLASE ORDER BY TAB POSITION
|
||||
import Status from '~/components/content/encounter/status.vue'
|
||||
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
|
||||
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
|
||||
// import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
|
||||
import PrescriptionList from '~/components/content/prescription/list.vue'
|
||||
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
|
||||
import DeviceOrder from '~/components/content/device-order/main.vue'
|
||||
import Prescription from '~/components/content/prescription/main.vue'
|
||||
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
|
||||
import Radiology from '~/components/content/radiology-order/main.vue'
|
||||
import Consultation from '~/components/content/consultation/list.vue'
|
||||
import Cprj from '~/components/content/cprj/entry.vue'
|
||||
import DocUploadList from '~/components/content/document-upload/list.vue'
|
||||
import GeneralConsentList from '~/components/content/general-consent/entry.vue'
|
||||
import SummaryMedic from '~/components/content/summary-medic/entry.vue'
|
||||
import ResumeList from '~/components/content/resume/list.vue'
|
||||
import ControlLetterList from '~/components/content/control-letter/list.vue'
|
||||
import InitialNursingStudy from '~/components/content/initial-nursing/entry.vue'
|
||||
// App Components
|
||||
import EncounterPatientInfo from '~/components/app/encounter/quick-info.vue'
|
||||
import EncounterHistoryButtonMenu from '~/components/app/encounter/quick-shortcut.vue'
|
||||
|
||||
///// Declarations and Flows
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
classCode: EncounterProps['classCode']
|
||||
subClassCode?: EncounterProps['subClassCode']
|
||||
canCreate?: boolean
|
||||
canRead?: boolean
|
||||
canUpdate?: boolean
|
||||
canDelete?: boolean
|
||||
}>()
|
||||
|
||||
// Common preparations
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// activeTab selalu sinkron dengan query param
|
||||
const activeTab = computed({
|
||||
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
|
||||
set: (val: string) => {
|
||||
router.replace({ path: route.path, query: { tab: val } })
|
||||
const { user, userActiveRole, getActiveRole } = useUserStore()
|
||||
const activeRole = getActiveRole()
|
||||
const activePosition = ref(getServicePosition(activeRole))
|
||||
const menus = ref([] as any)
|
||||
const activeMenu = computed({
|
||||
get: () => (route.query?.menu && typeof route.query.menu === 'string' ? route.query.menu : 'status'),
|
||||
set: (value: string) => {
|
||||
router.replace({ path: route.path, query: { menu: value } })
|
||||
},
|
||||
})
|
||||
|
||||
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const dataRes = await getDetail(id, { includes: 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person'})
|
||||
const dataResBody = dataRes.body ?? null
|
||||
const data = dataResBody?.data ?? null
|
||||
const data = ref<Encounter>(genEncounter())
|
||||
const isShowPatient = computed(() => data.value && data.value?.patient?.person)
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ value: 'status', label: 'Status Masuk/Keluar', component: Status, props: { encounter: data } },
|
||||
{ value: 'early-medical-assessment', label: 'Pengkajian Awal Medis', component: EarlyMedicalAssesmentList },
|
||||
const { setOpen } = useSidebar()
|
||||
setOpen(false)
|
||||
|
||||
if (activePosition.value === 'none') {
|
||||
// if user position is none, redirect to home page
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
|
||||
const protocolRows = [
|
||||
{
|
||||
number: '1',
|
||||
tanggal: new Date().toISOString().substring(0, 10),
|
||||
siklus: 'I',
|
||||
periode: 'Siklus I',
|
||||
kehadiran: 'Hadir',
|
||||
action: '',
|
||||
},
|
||||
{
|
||||
value: 'rehab-medical-assessment',
|
||||
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
|
||||
component: EarlyMedicalRehabList,
|
||||
props: { encounter: data, type: 'early-rehab', label: 'Pengkajian Awal Medis Rehabilitasi Medis' },
|
||||
},
|
||||
{
|
||||
value: 'function-assessment',
|
||||
label: 'Asesmen Fungsi',
|
||||
component: AssesmentFunctionList,
|
||||
props: { encounter: data, type: 'function', label: 'Asesmen Fungsi' },
|
||||
},
|
||||
{ value: 'function-assessment', label: 'Asesmen Fungsi', component: AssesmentFunctionList },
|
||||
{ value: 'therapy-protocol', label: 'Protokol Terapi' },
|
||||
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
|
||||
{ value: 'consent', label: 'General Consent' },
|
||||
{ value: 'patient-note', label: 'CPRJ' },
|
||||
{ value: 'prescription', label: 'Order Obat', component: PrescriptionList },
|
||||
{ value: 'patient-note', label: 'CPRJ', component: Cprj, props: { encounter: data } },
|
||||
{ value: 'summary-medic', label: 'Profil Ringkasan Medis', component: SummaryMedic, props: { encounter: data } },
|
||||
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.value.id } },
|
||||
{ value: 'device-order', label: 'Order Alkes', component: DeviceOrder, props: { encounter_id: data.value.id } },
|
||||
{ value: 'device', label: 'Order Alkes' },
|
||||
{ value: 'mcu-radiology', label: 'Order Radiologi' },
|
||||
{ value: 'mcu-lab-pc', label: 'Order Lab PK' },
|
||||
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
|
||||
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
|
||||
{ value: 'consent', label: 'General Consent', component: GeneralConsentList, props: { encounter: data } },
|
||||
{
|
||||
value: 'initial-nursing-study',
|
||||
label: 'Kajian Awal Keperawatan',
|
||||
component: InitialNursingStudy,
|
||||
props: { encounter: data },
|
||||
},
|
||||
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
|
||||
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
|
||||
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
|
||||
{ value: 'mcu-result', label: 'Hasil Penunjang' },
|
||||
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
|
||||
{ value: 'resume', label: 'Resume' },
|
||||
{ value: 'control', label: 'Surat Kontrol' },
|
||||
{ value: 'resume', label: 'Resume', component: ResumeList, props: { encounter: data } },
|
||||
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
|
||||
{ value: 'screening', label: 'Skrinning MPP' },
|
||||
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
|
||||
{
|
||||
value: 'supporting-document',
|
||||
label: 'Upload Dokumen Pendukung',
|
||||
component: DocUploadList,
|
||||
props: { encounter: data },
|
||||
number: '2',
|
||||
tanggal: new Date().toISOString().substring(0, 10),
|
||||
siklus: 'II',
|
||||
periode: 'Siklus II',
|
||||
kehadiran: 'Tidak Hadir',
|
||||
action: '',
|
||||
},
|
||||
]
|
||||
|
||||
const paginationMeta = {
|
||||
recordCount: protocolRows.length,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
}
|
||||
|
||||
// Reacrtivities
|
||||
watch(getActiveRole, () => {
|
||||
activePosition.value = getServicePosition(userActiveRole)
|
||||
initMenus()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getData()
|
||||
initMenus()
|
||||
})
|
||||
|
||||
///// Functions
|
||||
function handleClick(type: string) {
|
||||
if (type === 'draft') {
|
||||
router.push(`/${props.classCode}/encounter`)
|
||||
}
|
||||
}
|
||||
|
||||
function initMenus() {
|
||||
menus.value = getMenuItems(
|
||||
id,
|
||||
props,
|
||||
user,
|
||||
{
|
||||
encounter: data.value,
|
||||
} as any,
|
||||
{
|
||||
protocolTheraphy: paginationMeta,
|
||||
protocolChemotherapy: paginationMeta,
|
||||
medicineProtocolChemotherapy: paginationMeta,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
data.value = await getEncounterData(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-4">
|
||||
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
|
||||
<div class="bg-white dark:bg-slate-800">
|
||||
<div class="p-3 2xl:p-4 border-b border-slate-300 dark:border-slate-700">
|
||||
<ContentNavBa label="Kembali Ke Daftar" @click="handleClick" />
|
||||
</div>
|
||||
<ContentSwitcher :active="1" class="h-[130px] 2xl:h-[160px]">
|
||||
<template v-slot:content1>
|
||||
<EncounterPatientInfo v-if="isShowPatient" :data="data" />
|
||||
</template>
|
||||
<template v-slot:content2>
|
||||
<EncounterHistoryButtonMenu v-if="isShowPatient" />
|
||||
</template>
|
||||
</ContentSwitcher>
|
||||
</div>
|
||||
<AppEncounterQuickInfo :data="data" />
|
||||
<CompTab
|
||||
:data="tabs"
|
||||
:initial-active-tab="activeTab"
|
||||
@change-tab="activeTab = $event"
|
||||
<SubMenu
|
||||
:data="menus"
|
||||
:initial-active-menu="activeMenu"
|
||||
@change-menu="activeMenu = $event"
|
||||
:can-create="canCreate"
|
||||
:can-read="canRead"
|
||||
:can-update="canUpdate"
|
||||
:can-delete="canDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import { getValueLabelList as getDoctorValueLabelList } from '~/services/doctor.service'
|
||||
import { getValueLabelList as getEmployeeValueLabelList } from '~/services/employee.service'
|
||||
import { getValueLabelList as getUnitValueLabelList } from '~/services/unit.service'
|
||||
import type { CheckInFormData, CheckOutFormData } from '~/schemas/encounter.schema'
|
||||
import { CheckInSchema, CheckOutSchema } from '~/schemas/encounter.schema'
|
||||
import { getEncounterData } from '~/handlers/encounter-process.handler'
|
||||
|
||||
//
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import CheckInView from '~/components/app/encounter/check-in-view.vue'
|
||||
import CheckInEntry from '~/components/app/encounter/check-in-entry.vue'
|
||||
import CheckOutView from '~/components/app/encounter/check-out-view.vue'
|
||||
import CheckOutEntry from '~/components/app/encounter/check-out-entry.vue'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import { checkIn } from '~/services/encounter.service'
|
||||
|
||||
//
|
||||
const props = defineProps<{
|
||||
encounter: Encounter
|
||||
canUpdate?: boolean
|
||||
}>()
|
||||
|
||||
// doctors
|
||||
const localEncounter = ref<Encounter>(props.encounter)
|
||||
const doctors = await getDoctorValueLabelList({'includes': 'employee,employee-person'}, true)
|
||||
const employees = await getEmployeeValueLabelList({'includes': 'person', 'position-code': 'reg'})
|
||||
const units = await getUnitValueLabelList()
|
||||
|
||||
// check in
|
||||
const checkInValues = ref<any>({
|
||||
discharge_method_code: '',
|
||||
responsible_doctor_id: 0,
|
||||
// registeredAt: '',
|
||||
})
|
||||
const checkInIsLoading = ref(false)
|
||||
const checkInIsReadonly = ref(props.canUpdate )
|
||||
const checkInDialogOpen = ref(false)
|
||||
|
||||
// check out
|
||||
const checkOutValues = ref<any>({
|
||||
dischargeMethod_code: '',
|
||||
unit_id: 0,
|
||||
responsibleDoctor_id: 0,
|
||||
})
|
||||
const checkOutIsLoading = ref(false)
|
||||
const checkOutIsReadonly = ref(false)
|
||||
const checkOutDialogOpen = ref(false)
|
||||
|
||||
function editCheckIn() {
|
||||
checkInDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function submitCheckIn(values: CheckInFormData) {
|
||||
const res = await checkIn(props.encounter.id, values)
|
||||
if (res.success) {
|
||||
checkInDialogOpen.value = false
|
||||
const resEnc = await getEncounterData(props.encounter.id)
|
||||
if (resEnc.success) {
|
||||
localEncounter.value = resEnc.body.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editCheckOut() {
|
||||
checkOutDialogOpen.value = true
|
||||
}
|
||||
|
||||
function submitCheckOut(values: CheckOutFormData) {
|
||||
console.log(values)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lg:grid grid-cols-2">
|
||||
<div class="border-r lg:pe-4 xl:pe-5 mb-10">
|
||||
<div class="mb-4 xl:mb-5 text-base text-center font-semibold">Informasi Masuk</div>
|
||||
<CheckInView
|
||||
:encounter="localEncounter"
|
||||
:can-update="canUpdate"
|
||||
@edit="editCheckIn"
|
||||
/>
|
||||
</div>
|
||||
<div class="lg:ps-4 xl:ps-5">
|
||||
<Separator class="lg:hidden my-4 xl:my-5" />
|
||||
<div class="mb-4 xl:mb-5 text-base text-center font-semibold">Informasi Keluar</div>
|
||||
<CheckOutView
|
||||
:encounter="localEncounter"
|
||||
:can-update="canUpdate"
|
||||
@edit="editCheckOut"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:open="checkInDialogOpen"
|
||||
title="Ubah Informasi Masuk"
|
||||
size="md"
|
||||
prevent-outside
|
||||
>
|
||||
<CheckInEntry
|
||||
:schema="CheckInSchema"
|
||||
:values="checkInValues"
|
||||
:encounter="encounter"
|
||||
:doctors="doctors"
|
||||
:employees="employees"
|
||||
:is-loading="checkInIsLoading"
|
||||
:is-readonly="checkInIsReadonly"
|
||||
@submit="submitCheckIn"
|
||||
@cancel="checkInDialogOpen = false"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-model:open="checkOutDialogOpen"
|
||||
title="Ubah Informasi Keluar"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<CheckOutEntry
|
||||
:schema="CheckOutSchema"
|
||||
:values="checkOutValues"
|
||||
:encounter="encounter"
|
||||
:units="units"
|
||||
:doctors="doctors"
|
||||
:is-loading="checkInIsLoading"
|
||||
:is-readonly="checkInIsReadonly"
|
||||
@submit="submitCheckOut"
|
||||
@cancel="checkInDialogOpen = false"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -110,6 +110,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMaterialDetail(recId.value)
|
||||
title.value = 'Edit Perlengkapan'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -158,7 +159,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: MaterialFormData, resetForm: any) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getEquipmentList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getEquipmentList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getEquipmentList, resetForm, toast)
|
||||
@@ -173,7 +174,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getEquipmentList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getEquipmentList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useQueryMode } from '@/composables/useQueryMode'
|
||||
|
||||
import List from './list.vue'
|
||||
import Form from './form.vue'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const route = useRoute()
|
||||
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<List
|
||||
v-if="mode === 'list'"
|
||||
:encounter="props.encounter"
|
||||
@add="goToEntry"
|
||||
@edit="goToEntry"
|
||||
/>
|
||||
<Form
|
||||
v-else
|
||||
@back="backToList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/general-consent/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su-pr.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { GeneralConsentSchema } from '~/schemas/general-consent.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/general-consent.handler'
|
||||
import { create } from '~/services/generate-file.service'
|
||||
// Services
|
||||
import { getDetail } from '~/services/general-consent.service'
|
||||
const { backToList } = useQueryCRUDMode('mode')
|
||||
const { recordId } = useQueryCRUDRecordId('record-id')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const isOpenFungsional = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const fungsional = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const selectedFungsional = ref<any>(null)
|
||||
const schema = GeneralConsentSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
value: '',
|
||||
})
|
||||
const model = ref({
|
||||
relatives: [],
|
||||
responsibleName: '',
|
||||
responsiblePhone: '',
|
||||
informant: '',
|
||||
witness1: '',
|
||||
witness2: '',
|
||||
})
|
||||
|
||||
const fileUrl = ref('')
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const mode = route.query.mode
|
||||
const recordId = route.query['record-id']
|
||||
|
||||
if (mode === 'entry' && recordId) {
|
||||
loadEntryForEdit(+recordId)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: mapping data detail when edit
|
||||
const loadEntryForEdit = async (id: number) => {
|
||||
const result = await getDetail(id)
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.body?.data || {}
|
||||
|
||||
const value = JSON.parse(data.value || '{}')
|
||||
model.value.witness1 = value?.witness1 || ''
|
||||
model.value.witness2 = value?.witness2 || ''
|
||||
model.value.informant = value?.informant || ''
|
||||
model.value.responsibleName = value?.responsible || ''
|
||||
model.value.responsiblePhone = value?.responsiblePhone || ''
|
||||
model.value.relatives = value?.relatives || []
|
||||
console.log('model', model.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(type: string) {
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
} else if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
const entryGeneralConsent = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
if (type === 'print') {
|
||||
const data = await getDetail(recordId.value)
|
||||
const detail = data.body?.data
|
||||
fileUrl.value = detail?.fileUrl
|
||||
isOpenDiagnose.value = true
|
||||
return
|
||||
}
|
||||
const result = await entryGeneralConsent.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (result.data.relatives.length > 0) {
|
||||
result.data.relatives = result.data.relatives.map((item: any) => {
|
||||
return item.name
|
||||
})
|
||||
}
|
||||
|
||||
console.log('data', result)
|
||||
const resp = await handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
const data = resp.body?.data
|
||||
if (data) {
|
||||
const resp2 = await create({
|
||||
entityType_code: 'encounter',
|
||||
ref_id: data?.id,
|
||||
type_code: 'general-consent',
|
||||
})
|
||||
console.log('resp2', resp2.body?.data)
|
||||
backToList()
|
||||
}
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
// icdPreview.value.procedures = selectedProcedure.value || []
|
||||
// icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
// icdPreview.value.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="entryGeneralConsent"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Preview General Content"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<embed
|
||||
style="width: 100%; height: 90vh"
|
||||
:src="fileUrl"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/general-consent/list.vue'
|
||||
import Entry from '~/components/app/general-consent/entry.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { GeneralConsentSchema, type GeneralConsentFormData } from '~/schemas/general-consent.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/general-consent.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/general-consent.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
let units = ref<{ value: string; label: string }[]>([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({ 'encounter-id': props.encounter.id, includes: '', search, page })
|
||||
if (result.success) {
|
||||
data.value = result.body.data
|
||||
}
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'general-consent',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'General Consent',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
goToEntry()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const goEdit = (id: string) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch(recId, () => {
|
||||
console.log('recId', recId.value)
|
||||
if (recAction.value === ActionEvents.showEdit) {
|
||||
goEdit(recId.value)
|
||||
return
|
||||
} else {
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<List
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useQueryMode } from '@/composables/useQueryMode'
|
||||
|
||||
import List from './list.vue'
|
||||
import Form from './form.vue'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const route = useRoute()
|
||||
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<List
|
||||
v-if="mode === 'list'"
|
||||
:encounter="props.encounter"
|
||||
@add="goToEntry"
|
||||
@edit="goToEntry"
|
||||
/>
|
||||
<Form
|
||||
v-else
|
||||
@back="backToList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/initial-nursing/entry-form.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { InitialNursingSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const schema = InitialNursingSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
typeCode: 'early-nursery',
|
||||
value: '',
|
||||
})
|
||||
const listProblem = ref([])
|
||||
|
||||
const model = ref({
|
||||
'pri-complain': '',
|
||||
'med-type': '',
|
||||
'med-name': '',
|
||||
'med-reaction': '',
|
||||
'food-type': '',
|
||||
'food-name': '',
|
||||
'food-reaction': '',
|
||||
'other-type': '',
|
||||
'other-name': '',
|
||||
'other-reaction': '',
|
||||
'pain-asst': '',
|
||||
'pain-scale': '',
|
||||
'pain-time': '',
|
||||
'pain-duration': '',
|
||||
'pain-freq': '',
|
||||
'pain-loc': '',
|
||||
'nut-screening': '',
|
||||
'spiritual-asst': '',
|
||||
'general-condition': '',
|
||||
'support-exam': '',
|
||||
'risk-fall': '',
|
||||
bracelet: '',
|
||||
'bracelet-alg': '',
|
||||
})
|
||||
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
function handleOpen(event: any) {
|
||||
console.log('handleOpen', event.type)
|
||||
const type = event.type
|
||||
if (type === 'add-problem') {
|
||||
listProblem.value = event.data
|
||||
}
|
||||
}
|
||||
|
||||
const entryRehabRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRehabRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (listProblem.value?.length > 0) {
|
||||
result.data.listProblem = listProblem.value || []
|
||||
}
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
|
||||
backToList()
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="entryRehabRef"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="early-rehab"
|
||||
@click="handleOpen"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Fungsional"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/initial-nursing/list.vue'
|
||||
import Preview from '~/components/app/initial-nursing/preview.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/soapi-early.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
const route = useRoute()
|
||||
|
||||
const { recordId } = useQueryCRUDRecordId()
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
let units = ref<{ value: string; label: string }[]>([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
const id = route.params.id
|
||||
const descData = ref({})
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
'encounter-id': id,
|
||||
'type-code': 'early-nursery',
|
||||
includes: 'encounter',
|
||||
search,
|
||||
page,
|
||||
})
|
||||
console.log('masukkk', result)
|
||||
if (result.success) {
|
||||
data.value = result.body.data
|
||||
}
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'initial-nursing',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Kajian Awal Keperawatan',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
goToEntry()
|
||||
emits('add')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const mappedData = computed(() => {
|
||||
if (!data.value || data.value.length === 0) return []
|
||||
|
||||
const raw = data.value[0]
|
||||
|
||||
// Pastikan raw.value adalah string JSON
|
||||
let parsed: any = {}
|
||||
try {
|
||||
parsed = JSON.parse(raw.value || '{}')
|
||||
} catch (err) {
|
||||
console.error('JSON parse error:', err)
|
||||
return []
|
||||
}
|
||||
|
||||
// Ambil listProblem
|
||||
const list = parsed.listProblem || []
|
||||
const textData = parsed
|
||||
|
||||
// Untuk keamanan: pastikan selalu array
|
||||
if (!Array.isArray(list)) return []
|
||||
|
||||
return { list, textData }
|
||||
})
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Detail Konsultasi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
emits('edit')
|
||||
recordId.value = recId.value
|
||||
console.log('recordId', recId.value)
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<Preview :preview="mappedData.textData" />
|
||||
|
||||
<h2 class="my-3 p-1 font-semibold">C. Daftar Masalah Keperawatan</h2>
|
||||
<List :data="mappedData.list || []" />
|
||||
<!-- :pagination-meta="paginationMeta" -->
|
||||
<!-- @page-change="handlePageChange" -->
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import AppInstallationPositionList from '~/components/app/installation-position/list.vue'
|
||||
import AppInstallationPositionEntryForm from '~/components/app/installation-position/entry-form.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { InstallationPositionSchema, type InstallationPositionFormData } from '~/schemas/installation-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/installation-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/installation-position.service'
|
||||
import { getValueLabelList as getInstallationLabelList } from '~/services/installation.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const installations = ref<{ value: string | number; label: string }[]>([])
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getInstallationPositionList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'installation,Employee.Person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'installation-position',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Instalasi - Posisi',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah Posisi',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getCurrentInstallationDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
title.value = 'Detail Instalasi - Posisi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentInstallationDetail(recItem.value.code)
|
||||
title.value = 'Edit Instalasi - Posisi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
installations.value = await getInstallationLabelList({ sort: 'createdAt:asc', 'page-size': 100 }, true)
|
||||
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<AppInstallationPositionList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Instalasi - Posisi'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppInstallationPositionEntryForm
|
||||
:schema="InstallationPositionSchema"
|
||||
:employees="employees"
|
||||
:installations="installations"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: InstallationPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getInstallationPositionList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getInstallationPositionList, resetForm, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getInstallationPositionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
// Service
|
||||
import type { Installation } from '~/models/installation'
|
||||
import { getDetail as getDetailInstallation } from '~/services/installation.service'
|
||||
|
||||
// #region installtaion positions
|
||||
import { config } from '~/components/app/installation/detail/list.cfg'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// Types
|
||||
import { type InstallationPositionFormData, InstallationPositionSchema } from '~/schemas/installation-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/installation-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail as getDetailInstallationPosition } from '~/services/installation-position.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
|
||||
const title = ref('')
|
||||
// #endregion
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
installationId: string
|
||||
}>()
|
||||
const installation = ref<Installation>({} as Installation)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getInstallationPositionList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
'installation-id': props.installationId,
|
||||
includes: 'Employee.Person',
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
'page-no-limit': true,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'installation-position',
|
||||
})
|
||||
|
||||
const dataMap = computed(() => {
|
||||
return data.value.map((v, i) => {
|
||||
return {
|
||||
...v,
|
||||
index: i + 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Instalasi',
|
||||
icon: 'i-lucide-user',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah Jabatan',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await getDetailInstallation(props.installationId)
|
||||
if (result.success) {
|
||||
installation.value = result.body.data || {}
|
||||
}
|
||||
|
||||
const res = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
employees.value = res
|
||||
} catch (err) {
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
console.log(recId, recAction)
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
getDetailInstallationPosition(recItem.value.code)
|
||||
title.value = 'Edit Jabatan'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
|
||||
<AppInstallationDetail :installation="installation" />
|
||||
<div class="h-6"></div>
|
||||
|
||||
<AppInstallationDetailList
|
||||
:data="dataMap"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Jabatan'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppInstallationPositionEntryDetail
|
||||
:schema="InstallationPositionSchema"
|
||||
:installation-id="Number(installationId)"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: InstallationPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getInstallationPositionList, onResetState, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getInstallationPositionList, onResetState, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getInstallationPositionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const installationConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih encounter class (fhir7)',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Ambulatory', code: 'AMB' },
|
||||
{ value: '2', label: 'Inpatient', code: 'IMP' },
|
||||
{ value: '3', label: 'Emergency', code: 'EMER' },
|
||||
{ value: '4', label: 'Observation', code: 'OBSENC' },
|
||||
{ value: '5', label: 'Pre-admission', code: 'PRENC' },
|
||||
{ value: '6', label: 'Short Stay', code: 'SS' },
|
||||
{ value: '7', label: 'Virtual', code: 'VR' },
|
||||
{ value: '8', label: 'Home Health', code: 'HH' },
|
||||
],
|
||||
}
|
||||
|
||||
export const schemaConf = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: 'Nama instalasi harus diisi',
|
||||
})
|
||||
.min(3, 'Nama instalasi minimal 3 karakter'),
|
||||
|
||||
code: z
|
||||
.string({
|
||||
required_error: 'Kode instalasi harus diisi',
|
||||
})
|
||||
.min(3, 'Kode instalasi minimal 3 karakter'),
|
||||
|
||||
encounterClassCode: z
|
||||
.string({
|
||||
required_error: 'Kelompok encounter class harus dipilih',
|
||||
})
|
||||
.min(1, 'Kelompok encounter class harus dipilih'),
|
||||
})
|
||||
@@ -102,12 +102,23 @@ const getCurrentInstallationDetail = async (id: number | string) => {
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentInstallationDetail(recId.value)
|
||||
title.value = 'Detail Instalasi'
|
||||
isReadonly.value = true
|
||||
if (recItem.value.code.length > 0) {
|
||||
navigateTo({
|
||||
name: 'org-src-installation-id',
|
||||
params: {
|
||||
id: recItem.value.code,
|
||||
},
|
||||
})
|
||||
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = false
|
||||
isReadonly.value = false
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentInstallationDetail(recId.value)
|
||||
getCurrentInstallationDetail(recItem.value.code)
|
||||
title.value = 'Edit Instalasi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
@@ -157,8 +168,8 @@ onMounted(async () => {
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: InstallationFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getInstallationList, resetForm, toast)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getInstallationList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getInstallationList, resetForm, toast)
|
||||
@@ -173,7 +184,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getInstallationList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getInstallationList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { KfrSchema, } from '~/schemas/kfr.schema'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/kfr.handler'
|
||||
import { getDetail } from '~/services/kfr.service';
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const route = useRoute()
|
||||
const mode = computed(() => props.record_id ? `edit` : `add`)
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
if(mode.value === `edit`) init()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function init(){
|
||||
const result = await getDetail(props.record_id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
inputForm.value?.setValues(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const inputData: any = await composeFormData()
|
||||
const response = mode.value === `add`
|
||||
? await handleActionSave(
|
||||
inputData,
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
: await handleActionEdit(
|
||||
props.record_id,
|
||||
inputData,
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
|
||||
const data = (response?.body?.data ?? null)
|
||||
if (!data) return
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = input?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) return navigateTo(props.callbackUrl)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
|
||||
const initial = {
|
||||
// subjective: '',
|
||||
// objective: '',
|
||||
// assesment: '',
|
||||
// planningGoal: '',
|
||||
// planningAction: '',
|
||||
// planningEducation: '',
|
||||
planningFrequency: 2,
|
||||
followUpPlan: 'EVALUASI',
|
||||
// followUpPlanDesc: '',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">
|
||||
Formulir Rawat Jalan KFR
|
||||
</div>
|
||||
<AppKfrEntry
|
||||
ref="inputForm"
|
||||
:schema="KfrSchema"
|
||||
:initial-values="initial"
|
||||
/>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,368 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Imports
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
|
||||
import { getList, remove } from '~/services/kfr.service'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import { cn } from '~/lib/utils'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import { VerificationSchema } from '~/schemas/verification.schema'
|
||||
import { handleActionSave } from '~/handlers/kfr.handler'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { CalendarDate } from '@internationalized/date'
|
||||
import type { DateRange } from 'radix-vue'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
// #endregion
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
}>(), {
|
||||
redirectToForm: () => { }
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const { getActiveRole } = useUserStore()
|
||||
const isVerifyDialogOpen = ref(false)
|
||||
const isValidateDialogOpen = ref(false)
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
const isPatientInTherapy = ref(false)
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params }),
|
||||
entityName: 'kfr',
|
||||
})
|
||||
const historyData = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params }),
|
||||
entityName: 'kfr-history',
|
||||
})
|
||||
|
||||
const dummy = [
|
||||
{
|
||||
id: 11,
|
||||
date: new Date(),
|
||||
result: {
|
||||
s: `Example`,
|
||||
o: `Example`,
|
||||
a: `Example`,
|
||||
p: {
|
||||
goal: `Example`,
|
||||
action: `Example`,
|
||||
education: `Example`,
|
||||
frequency: `Example`,
|
||||
},
|
||||
plan: `Example`,
|
||||
planDesc: `Description`,
|
||||
},
|
||||
type: `Asesmen`,
|
||||
status: {
|
||||
verified: 1,
|
||||
validated: 0,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref<number>(0)
|
||||
|
||||
const isAssessment = ref<boolean>(false)
|
||||
const isTherapyProtocol = ref<boolean>(false)
|
||||
const isReassessment = ref<boolean>(true)
|
||||
const isInOrBeyondAssessmentPeriod = ref<boolean>(true)
|
||||
|
||||
const isDoctor = computed(() => getActiveRole() === 'emp|doc')
|
||||
const isAdmin = computed(() => getActiveRole() === 'system')
|
||||
|
||||
const isCaptchaValid = ref(false)
|
||||
provide('isCaptchaValid', isCaptchaValid)
|
||||
|
||||
const addBtnTxt = computed(() => {
|
||||
if (isAssessment.value) {
|
||||
return `Tambah Asesmen`
|
||||
} else if (isTherapyProtocol.value) {
|
||||
return `Tambah Protokol Terapi`
|
||||
} else if (isReassessment.value) {
|
||||
return `Tambah Re-Asesmen`
|
||||
}
|
||||
return `Tambah Asesmen`
|
||||
})
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Formulir Rawat Jalan KFR",
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
if(isDoctor.value || isAdmin.value) {
|
||||
headerPrep.addNav = {
|
||||
label: addBtnTxt.value,
|
||||
onClick: () => {
|
||||
props.redirectToForm()
|
||||
},
|
||||
}
|
||||
}
|
||||
if(!isAssessment.value) {
|
||||
headerPrep.components = [
|
||||
{
|
||||
component: defineAsyncComponent(() => import('~/components/app/kfr/_common/btn-history.vue')),
|
||||
props: { }
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
const defaultDate = {
|
||||
start: new CalendarDate(2025, 1, 20),
|
||||
end: new CalendarDate(2025, 1, 20).add({ days: 20 }),
|
||||
}
|
||||
const historyDateValue = ref(defaultDate) as Ref<DateRange>
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
getPatientSummary()
|
||||
|
||||
const isInTherapy = false // TODO: determine if patient is in therapy
|
||||
handleIsPatientInTherapy(isInTherapy)
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function handleIsInAssesmentPeriood(value: boolean) {
|
||||
if (value) {
|
||||
isInOrBeyondAssessmentPeriod.value = true
|
||||
} else {
|
||||
isInOrBeyondAssessmentPeriod.value = false
|
||||
}
|
||||
}
|
||||
function handleIsPatientInTherapy(value: boolean) {
|
||||
if (value) {
|
||||
isPatientInTherapy.value = true
|
||||
} else {
|
||||
isPatientInTherapy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenHistory() {
|
||||
isHistoryDialogOpen.value = true
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHistoryDialog() {
|
||||
isHistoryDialogOpen.value = !isHistoryDialogOpen.value
|
||||
}
|
||||
|
||||
function handleVerify() {
|
||||
isVerifyDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleConfirmVerify() {
|
||||
const inputData: any = await composeFormData()
|
||||
const response = await handleActionSave(
|
||||
inputData,
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
|
||||
const data = (response?.body?.data ?? null)
|
||||
if (!data) return
|
||||
isVerifyDialogOpen.value = false
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = input?.values
|
||||
// formData.encounter_id = encounterId
|
||||
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
|
||||
async function handleConfirmValidate() {
|
||||
try {
|
||||
// const result = await remove(record.id)
|
||||
// if (result.success) {
|
||||
// toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
// await fetchData()
|
||||
// } else {
|
||||
// toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
// }
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelValidate() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
}
|
||||
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
await fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
handleConfirmVerify()
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
isVerifyDialogOpen.value = false
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Provide
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('timestamp', timestamp)
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('isHistoryDialogOpen', isHistoryDialogOpen)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
// if(pagePermission.canUpdate) {
|
||||
props.redirectToForm(recId.value)
|
||||
// } else {
|
||||
// unauthorizedToast()
|
||||
// }
|
||||
break
|
||||
case ActionEvents.showVerify:
|
||||
// if(pagePermission.canUpdate) {
|
||||
handleVerify()
|
||||
// } else {
|
||||
// unauthorizedToast()
|
||||
// }
|
||||
break
|
||||
case ActionEvents.showValidate:
|
||||
// if(pagePermission.canUpdate) {
|
||||
isValidateDialogOpen.value = true
|
||||
// } else {
|
||||
// unauthorizedToast()
|
||||
// }
|
||||
break
|
||||
case ActionEvents.showPrint:
|
||||
isDocPreviewDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<AppKfrCommonBannerPatientInTherapy v-if="isInOrBeyondAssessmentPeriod" class="mb-5" />
|
||||
|
||||
<AppKfrList :data="dummy" />
|
||||
|
||||
<Dialog v-model:open="isVerifyDialogOpen" title="Verifikasi">
|
||||
<AppKfrVerifyDialog ref="inputForm" :schema="VerificationSchema" />
|
||||
<div class="flex justify-end">
|
||||
<Action v-show="isCaptchaValid" :enable-draft="false" @click="handleActionClick" />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Confirmation
|
||||
v-model:open="isValidateDialogOpen"
|
||||
title="Validasi Data"
|
||||
message="Apakah Anda yakin ingin Validasi data ini?"
|
||||
confirm-text="Simpan"
|
||||
@confirm="handleConfirmValidate"
|
||||
@cancel="handleCancelValidate"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
|
||||
<AppKfrHistoryList
|
||||
:data="dummy"
|
||||
v-model:date-value="historyDateValue"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange" />
|
||||
</Dialog>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<!-- <DocPreviewDialog :link="recItem.url" /> -->
|
||||
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list"
|
||||
:encounter_id="encounter_id"
|
||||
:redirectToForm="goToEntry"
|
||||
:redirectToDetail="goToDetail"/>
|
||||
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
|
||||
</template>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
|
||||
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
|
||||
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
|
||||
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// mcu src category
|
||||
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
|
||||
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
|
||||
|
||||
// mcu src
|
||||
import { type McuSrc } from '~/models/mcu-src'
|
||||
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
|
||||
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
|
||||
|
||||
// mcu order
|
||||
import { getDetail } from '~/services/mcu-order.service'
|
||||
import Detail from '~/components/app/mcu-order/detail.vue'
|
||||
|
||||
// mcu order item, manually not using composable
|
||||
import {
|
||||
getList as getMcuOrderItemList,
|
||||
create as createMcuOrderItem,
|
||||
remove as removeMcuOrderItem,
|
||||
} from '~/services/mcu-order-item.service'
|
||||
import { type McuOrderItem } from '~/models/mcu-order-item'
|
||||
import ItemListEntry from '~/components/app/mcu-order-item/list-entry.vue'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
scopeCode: string
|
||||
}>()
|
||||
|
||||
// declaration & flows
|
||||
|
||||
// MCU Order
|
||||
const { backToList, crudQueryParams } = useQueryCRUD()
|
||||
const id = crudQueryParams.value.recordId
|
||||
const dataRes = await getDetail(
|
||||
typeof id === 'string' ? parseInt(id) : 0,
|
||||
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
|
||||
)
|
||||
const data = dataRes.body?.data
|
||||
|
||||
// MCU items
|
||||
const items = ref<McuOrderItem[]>([])
|
||||
|
||||
// MCU Categories
|
||||
const mcuSrcCategoryRes = await getMcuCategoryList({ 'scope-code': props.scopeCode })
|
||||
const mcuSrcCategories = mcuSrcCategoryRes.body?.data
|
||||
const selectedMcuSrcCategory_code = ref('')
|
||||
|
||||
// MCU Sources
|
||||
const mcuSrcs = ref<McuSrc[]>([])
|
||||
|
||||
// const {
|
||||
// data: items,
|
||||
// fetchData: getItems,
|
||||
// } = usePaginatedList<McuOrderItem> ({
|
||||
// fetchFn: async ({ page, search }) => {
|
||||
// const result = await getMcuOrderItemList({ 'mcu-order-id': id, search, page })
|
||||
// if (result.success) {
|
||||
// items.value = result.body.data
|
||||
// }
|
||||
// return { success: result.success || false, body: result.body || {} }
|
||||
// },
|
||||
// entityName: 'mcu-order-item',
|
||||
// })
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail dan List Item Order Radiologi ',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
const pickerDialogOpen = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await getItems()
|
||||
})
|
||||
|
||||
watch(selectedMcuSrcCategory_code, async () => {
|
||||
const res = await getMcuSrcList({ 'mcu-src-category-code': selectedMcuSrcCategory_code.value })
|
||||
mcuSrcs.value = res.body?.data
|
||||
})
|
||||
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
function requestItem() {
|
||||
pickerDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function pickItem(item: McuSrc) {
|
||||
const exItem = items.value.find(e => e.mcuSrc_code === item.code)
|
||||
if (exItem) {
|
||||
await removeMcuOrderItem(exItem.id)
|
||||
await getItems()
|
||||
} else {
|
||||
const intId = parseInt(id?.toString() || '0')
|
||||
await createMcuOrderItem({
|
||||
mcuOrder_id: intId,
|
||||
mcuSrc_code: item.code,
|
||||
})
|
||||
await getItems()
|
||||
}
|
||||
}
|
||||
|
||||
async function getItems() {
|
||||
const itemsRes = await getMcuOrderItemList({ 'mcu-order-id': id, includes: 'mcuSrc,mcuSrc-mcuSrcCategory' })
|
||||
if (itemsRes.success) {
|
||||
items.value = itemsRes.body.data
|
||||
} else {
|
||||
items.value = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<Detail :data="data" />
|
||||
|
||||
<ItemListEntry
|
||||
:data="items"
|
||||
@requestItem="requestItem"/>
|
||||
<Separator class="my-5" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<Nav @click="navClick" />
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:open="pickerDialogOpen"
|
||||
title="Pilih Item"
|
||||
size="2xl"
|
||||
prevent-outside
|
||||
>
|
||||
<ScrCategorySwitcher :data="mcuSrcCategories" v-model="selectedMcuSrcCategory_code" />
|
||||
<McuSrcPicker v-model="items" :data-source="mcuSrcs" @pick="pickItem" />
|
||||
<Separator />
|
||||
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Handlers
|
||||
import type { ToastFn } from '~/handlers/_handler'
|
||||
|
||||
// Apps
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionSave,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/mcu-order.handler'
|
||||
import { getList, getDetail, submit } from '~/services/mcu-order.service'
|
||||
import type { McuOrder } from '~/models/mcu-order'
|
||||
// import List from '~/components/app/mcu-order/micro-list.vue'
|
||||
import ConfirmationInfo from '~/components/app/mcu-order/confirmation-info.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
scopeCode: string
|
||||
}>()
|
||||
|
||||
// Common preparations
|
||||
const route = useRoute()
|
||||
const { crudQueryParams } = useQueryCRUD()
|
||||
|
||||
const title = ref('')
|
||||
const plainEid = route.params.id
|
||||
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
|
||||
// Data
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList<McuOrder>({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
search,
|
||||
page,
|
||||
'scope-code': props.scopeCode,
|
||||
'encounter-id': encounter_id,
|
||||
includes: 'doctor,doctor-employee,doctor-employee-person,items,items-mcuSrc',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'mcu-order'
|
||||
})
|
||||
|
||||
// Header
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Order Lab Mikro',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: async () => {
|
||||
const saveResp = await handleActionSave({ encounter_id, scope_code: props.scopeCode }, () => {}, () =>{}, toast)
|
||||
if (saveResp.success) {
|
||||
crudQueryParams.value = { mode: 'entry', recordId: saveResp.body?.data?.id.toString() }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// List component
|
||||
let List: Component
|
||||
if(props.scopeCode == 'radiology') {
|
||||
List = defineAsyncComponent(() => import('~/components/content/encounter/status.vue'))
|
||||
} else if(props.scopeCode == 'cp-lab') {
|
||||
List = defineAsyncComponent(() => import('~/components/content/encounter/status.vue'))
|
||||
} else if (props.scopeCode == 'micro-lab') {
|
||||
List = defineAsyncComponent(() => import('~/components/app/mcu-order/micro-list.vue'))
|
||||
} else {
|
||||
List = defineAsyncComponent(() => import('~/components/content/encounter/status.vue'))
|
||||
}
|
||||
|
||||
// Reactivities
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Detail Order Lab PK'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Edit Order Lab PK'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
///// Functions
|
||||
async function getMyDetail (id: number | string) {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(data: McuOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
function edit(data: McuOrder) {
|
||||
crudQueryParams.value = { mode: 'entry', recordId: data.id.toString() }
|
||||
}
|
||||
|
||||
function confirmSubmit(data: McuOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isSubmitConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
async function handleActionSubmit(id: number, refresh: () => void, toast: ToastFn) {
|
||||
const result = await submit(id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
setTimeout(refresh, 300)
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: 'Gagal menjalankan perintah', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<List
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="cancel"
|
||||
@edit="edit"
|
||||
@submit="confirmSubmit"
|
||||
/>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<ConfirmationInfo :rec-item="recItem" />
|
||||
</RecordConfirmation>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
action="delete"
|
||||
customTitle="Ajukan Resep"
|
||||
customMessage="Proses akan mengajukan resep ini untuk diproses lebih lanjut. Lanjutkan?"
|
||||
customConfirmText="Ajukan"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionSubmit(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<ConfirmationInfo :rec-item="recItem" />
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import AppMedicineFormList from '~/components/app/medicine-form/list.vue'
|
||||
import AppMedicineFormEntryForm from '~/components/app/medicine-form/entry-form.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { BaseSchema, type BaseFormData } from '~/schemas/base.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/medicine-form.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/medicine-form.service'
|
||||
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMedicineFormList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'medicine-form',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Sediaan Obat',
|
||||
icon: 'i-lucide-medicine-bottle',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getCurrentMedicineFormDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentMedicineFormDetail(recId.value)
|
||||
title.value = 'Detail Sediaan Obat'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentMedicineFormDetail(recId.value)
|
||||
title.value = 'Edit Sediaan Obat'
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMedicineFormList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<AppMedicineFormList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Sediaan Obat'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppMedicineFormEntryForm
|
||||
:schema="BaseSchema"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recItem.code, values, getMedicineFormList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineFormList, resetForm, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineFormList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMedicineGroupDetail(recId.value)
|
||||
title.value = 'Edit Kelompok Obat'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineGroupList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineGroupList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineGroupList, resetForm, toast)
|
||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineGroupList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineGroupList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -108,6 +108,7 @@ watch([recId, recAction], () => {
|
||||
getCurrentMedicineMethodDetail(recId.value)
|
||||
title.value = 'Edit Metode Obat'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
@@ -154,7 +155,7 @@ onMounted(async () => {
|
||||
@submit="
|
||||
(values: BaseFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineMethodList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineMethodList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineMethodList, resetForm, toast)
|
||||
@@ -169,7 +170,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineMethodList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineMethodList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -37,10 +37,12 @@ import {
|
||||
import { getList, getDetail } from '~/services/medicine.service'
|
||||
import { getValueLabelList as getMedicineGroupList } from '~/services/medicine-group.service'
|
||||
import { getValueLabelList as getMedicineMethodList } from '~/services/medicine-method.service'
|
||||
import { getValueLabelList as getMedicineFormList } from '~/services/medicine-form.service'
|
||||
import { getValueLabelList as getUomList } from '~/services/uom.service'
|
||||
|
||||
const medicineGroups = ref<{ value: string; label: string }[]>([])
|
||||
const medicineMethods = ref<{ value: string; label: string }[]>([])
|
||||
const medicineForms = ref<{ value: string; label: string }[]>([])
|
||||
const uoms = ref<{ value: string; label: string }[]>([])
|
||||
const title = ref('')
|
||||
|
||||
@@ -59,7 +61,7 @@ const {
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'medicineGroup,medicineMethod,uom',
|
||||
includes: 'medicineGroup,medicineMethod,medicineForm,uom',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
@@ -116,6 +118,7 @@ watch([recId, recAction], () => {
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentMedicineDetail(recId.value)
|
||||
title.value = 'Edit Obat'
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
@@ -127,6 +130,7 @@ watch([recId, recAction], () => {
|
||||
onMounted(async () => {
|
||||
medicineGroups.value = await getMedicineGroupList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
medicineMethods.value = await getMedicineMethodList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
medicineForms.value = await getMedicineFormList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
uoms.value = await getUomList({ sort: 'createdAt:asc', 'page-size': 100 })
|
||||
await getMedicineList()
|
||||
})
|
||||
@@ -163,13 +167,14 @@ onMounted(async () => {
|
||||
:values="recItem"
|
||||
:medicineGroups="medicineGroups"
|
||||
:medicineMethods="medicineMethods"
|
||||
:medicineForms="medicineForms"
|
||||
:uoms="uoms"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: MedicineFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getMedicineList, resetForm, toast)
|
||||
handleActionEdit(recItem.code, values, getMedicineList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getMedicineList, resetForm, toast)
|
||||
@@ -184,7 +189,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMedicineList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getMedicineList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import Entry from '~/components/content/mcu-order/entry.vue'
|
||||
|
||||
defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Entry :encounter_id="encounter_id" scope-code="micro-lab" />
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import List from '~/components/content/mcu-order/list.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List scope-code="micro-lab" />
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Imports
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { getList, remove } from '~/services/prb.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
|
||||
import type { Prb } from '~/models/prb'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter?: Encounter
|
||||
isBpjs?: boolean
|
||||
}>(), {
|
||||
isBpjs: false,
|
||||
})
|
||||
const route = useRoute()
|
||||
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: '', }),
|
||||
entityName: 'prb',
|
||||
})
|
||||
const prbHistory = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params }),
|
||||
entityName: 'prb-history',
|
||||
})
|
||||
const dummy = [
|
||||
{
|
||||
"id": 1,
|
||||
"date": new Date().toISOString(),
|
||||
"name1": "Dr. Smith",
|
||||
"name2": "Maria S.",
|
||||
"name3": "Project Alpha",
|
||||
"name4": "Completed",
|
||||
"name5": 95.5
|
||||
},
|
||||
]
|
||||
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const isFilterDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
const isRequirementsMet = ref(true)
|
||||
const Prb = ref<Prb | null>(null)
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref<any>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Program Rujuk Balik",
|
||||
icon: 'i-lucide-history',
|
||||
}
|
||||
if(true){
|
||||
headerPrep.addNav = {
|
||||
label: "Program Rujuk Balik",
|
||||
onClick: () => navigateTo({
|
||||
name: 'integration-bpjs-prb-add',
|
||||
}),
|
||||
};
|
||||
}
|
||||
headerPrep.components = [
|
||||
{
|
||||
component: defineAsyncComponent(() => import('~/components/app/prb/_common/btn-history.vue')),
|
||||
props: { }
|
||||
},
|
||||
];
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
isFilterDialogOpen.value = true
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
getListData()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function getListData() {
|
||||
try {
|
||||
summaryLoading.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
console.error('Error fetching Data:', error)
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
await fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelConfirmation() {
|
||||
// Reset record state when cancelled
|
||||
recId.value = 0
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
}
|
||||
|
||||
function handleFiltering() {
|
||||
isFilterDialogOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Provide
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('timestamp', timestamp)
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('isHistoryDialogOpen', isHistoryDialogOpen)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
navigateTo({
|
||||
name: 'integration-bpjs-prb-prb_id-edit',
|
||||
params: { id: encounterId, "prb_id": recId.value },
|
||||
})
|
||||
break
|
||||
|
||||
case ActionEvents.showPrint:
|
||||
isDocPreviewDialogOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WarningAlert v-if="!isRequirementsMet"
|
||||
class="mb-5"
|
||||
text="Syarat pembuatan PRB belum terpenuhi"
|
||||
:description="[
|
||||
'Lanjutan Penatalaksanaan Pasien harus terisi Dirujuk Eksternal',
|
||||
'Jenis Pembayaran pasien harus JKN'
|
||||
]" />
|
||||
|
||||
<div v-else>
|
||||
<Header :prep="headerPrep" />
|
||||
|
||||
<Filter
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="refSearchNav"
|
||||
:enable-export="false"
|
||||
/>
|
||||
|
||||
<AppPrbList :is-bpjs="true" :data="dummy" />
|
||||
|
||||
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
|
||||
<AppPrbHistoryList
|
||||
:data="dummy"
|
||||
:pagination-meta="prbHistory.paginationMeta"
|
||||
@page-change="prbHistory.handlePageChange" />
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="isFilterDialogOpen" title="Filter" size="lg">
|
||||
<AppPrbCommonFilter @submit="handleFiltering" />
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||
</Dialog>
|
||||
|
||||
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.firstName">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.firstName }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.cellphone }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<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'
|
||||
import { getDetail } from '~/services/prb.service'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import type { Prb } from '~/models/prb'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
|
||||
const router = useRouter()
|
||||
const Prb = ref<Prb | null>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Program Rujuk Balik',
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
// onMounted(async () => {
|
||||
// const result = await getDetail(controlLetterId, {
|
||||
// includes: "unit,specialist,subspecialist,doctor-employee-person",
|
||||
// })
|
||||
// if (result.success) {
|
||||
// controlLetter.value = result.body?.data
|
||||
// }
|
||||
// })
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
function handleAction() {
|
||||
goBack()
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
|
||||
<AppPrbDetail :instance="Prb" @click="handleAction" />
|
||||
</template>
|
||||
@@ -0,0 +1,148 @@
|
||||
<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 { getDetail, update } from '~/services/prb.service'
|
||||
import type { Prb } from '~/models/prb'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { PrbSchema } from '~/schemas/prb.schema'
|
||||
import { formatDateYyyyMmDd } from '~/lib/date'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { getList, remove } from '~/services/prb.service'
|
||||
import { handleActionEdit } from '~/handlers/prb.handler'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import { formatDateToDatetimeLocal } from '~/lib/utils'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
isBpjs?: boolean
|
||||
}>(), {
|
||||
isBpjs: false,
|
||||
})
|
||||
|
||||
// form related state
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
|
||||
entityName: 'prb',
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
const Prb = ref({})
|
||||
const isConfirmationOpen = ref(false)
|
||||
const selectedOperativeAction = ref<any>(null)
|
||||
|
||||
const isSepDialogOpen = ref(false)
|
||||
provide("isSepDialogOpen", isSepDialogOpen);
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(props.record_id)
|
||||
if (result.success) {
|
||||
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
|
||||
Prb.value = responseData
|
||||
inputForm.value?.setValues(responseData)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const response = await handleActionEdit(
|
||||
props.record_id,
|
||||
await composeFormData(),
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<Prb> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = input?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
|
||||
const initialValues = {
|
||||
a1: "",
|
||||
a2: formatDateToDatetimeLocal(new Date()),
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Update Program Rujuk Balik</div>
|
||||
<AppPrbEntry
|
||||
ref="inputForm"
|
||||
:schema="PrbSchema"
|
||||
:initial-values="initialValues"
|
||||
:is-bpjs="isBpjs"
|
||||
/>
|
||||
|
||||
<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,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Imports
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { getList, remove } from '~/services/prb.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
|
||||
import type { Prb } from '~/models/prb'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
isBpjs?: boolean
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
redirectToDetail?: (myRecord_id: string|number) => void
|
||||
}>(), {
|
||||
isBpjs: false,
|
||||
redirectToForm: () => { },
|
||||
redirectToDetail: () => { }
|
||||
})
|
||||
const { user } = useUserStore()
|
||||
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: '', }),
|
||||
entityName: 'prb',
|
||||
})
|
||||
const prbHistory = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params }),
|
||||
entityName: 'prb-history',
|
||||
})
|
||||
const dummy = [
|
||||
{
|
||||
"id": 1,
|
||||
"date": new Date().toISOString(),
|
||||
"name1": "Dr. Smith",
|
||||
"name2": "Maria S.",
|
||||
"name3": "Project Alpha",
|
||||
"name4": "Completed",
|
||||
"name5": 95.5
|
||||
},
|
||||
]
|
||||
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
const isRequirementsMet = ref(true)
|
||||
const Prb = ref<Prb | null>(null)
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const timestamp = ref<any>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Program Rujuk Balik",
|
||||
icon: 'i-lucide-history',
|
||||
}
|
||||
if(true){
|
||||
headerPrep.addNav = {
|
||||
label: "Program Rujuk Balik",
|
||||
onClick: () => {
|
||||
props.redirectToForm()
|
||||
},
|
||||
};
|
||||
}
|
||||
headerPrep.components = [
|
||||
{
|
||||
component: defineAsyncComponent(() => import('~/components/app/prb/_common/btn-history.vue')),
|
||||
props: { }
|
||||
},
|
||||
];
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
getListData()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function getListData() {
|
||||
try {
|
||||
summaryLoading.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
console.error('Error fetching Data:', error)
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
await fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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('timestamp', timestamp)
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('isHistoryDialogOpen', isHistoryDialogOpen)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction, timestamp], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
props.redirectToForm(recId.value)
|
||||
break
|
||||
|
||||
case ActionEvents.showPrint:
|
||||
isDocPreviewDialogOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WarningAlert v-if="!isRequirementsMet"
|
||||
class="mb-5"
|
||||
text="Syarat pembuatan PRB belum terpenuhi"
|
||||
:description="[
|
||||
'Lanjutan Penatalaksanaan Pasien harus terisi Dirujuk Eksternal',
|
||||
'Jenis Pembayaran pasien harus JKN'
|
||||
]" />
|
||||
|
||||
<div v-else>
|
||||
<Header :prep="headerPrep" />
|
||||
|
||||
<AppPrbDetail :instance="Prb" />
|
||||
<h1 class="font-semibold text-lg mb-2">Obat</h1>
|
||||
<AppPrbList :is-bpjs="isBpjs" :data="dummy" />
|
||||
|
||||
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
|
||||
<AppPrbHistoryList
|
||||
:data="dummy"
|
||||
:pagination-meta="prbHistory.paginationMeta"
|
||||
@page-change="prbHistory.handlePageChange" />
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||
</Dialog>
|
||||
|
||||
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.firstName">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.firstName }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.cellphone }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Detail from './detail.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list && !crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:redirectToForm="goToEntry"
|
||||
:redirectToDetail="goToDetail"/>
|
||||
<Detail v-else-if="crudQueryParams.mode === crudQueryParamsMode.list && crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId"
|
||||
:redirectToForm="goToEntry"/>
|
||||
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
|
||||
</template>
|
||||
@@ -0,0 +1,366 @@
|
||||
<script setup lang="ts">
|
||||
import { type HeaderPrep, ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
|
||||
|
||||
// Medicine
|
||||
import { type Medicine } from '~/models/medicine'
|
||||
import { getList as getMedicineList } from '~/services/medicine.service'
|
||||
|
||||
// Prescription
|
||||
import { getDetail } from '~/services/prescription.service'
|
||||
// import Detail from '~/components/app/prescription/detail.vue'
|
||||
import Entry from '~/components/app/prescription/entry.vue'
|
||||
import { remove, submit } from '~/services/prescription.service'
|
||||
|
||||
// Prescription items
|
||||
import {
|
||||
getList as getItemList,
|
||||
create as createItem,
|
||||
update as updateItem,
|
||||
} from '~/services/prescription-item.service'
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/prescription-item.handler'
|
||||
|
||||
import { type PrescriptionItem, genPrescriptionItem } from '~/models/prescription-item'
|
||||
import { type MedicinemixItem } from '~/models/medicinemix-item';
|
||||
import ItemListEntry from '~/components/app/prescription-item/list-entry.vue'
|
||||
import NonMixItemEntry from '~/components/app/prescription-item/non-mix-entry.vue'
|
||||
import MixItemEntry from '~/components/app/prescription-item/mix-entry.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
// Prescriptions
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
const isDeleteConfirmationOpen = ref(false)
|
||||
|
||||
// Prescription Items
|
||||
const isItemDetailDialogOpen = ref(false)
|
||||
|
||||
// Prescription
|
||||
const { getQueryParam } = useQueryParam()
|
||||
const rawId = getQueryParam('id')
|
||||
const id = typeof rawId === 'string' ? parseInt(rawId) : 0
|
||||
const dataRes = await getDetail(id, { includes: 'encounter,doctor,doctor-employee,doctor-employee-person' })
|
||||
const data = ref(dataRes.body?.data || null)
|
||||
|
||||
// Prescription Items
|
||||
const getItemsOpt = { 'prescription-id': id, includes: 'medicine,medicine-medicineForm,medicineMix' }
|
||||
const {
|
||||
data: items,
|
||||
isLoading,
|
||||
fetchData: getPrescriptionItems,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getItemList({
|
||||
...getItemsOpt,
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'division',
|
||||
})
|
||||
|
||||
|
||||
// const items = ref<PrescriptionItem[]>([])
|
||||
const mixItem = ref<PrescriptionItem>(genPrescriptionItem())
|
||||
const medicinemixItems = ref<MedicinemixItem[]>([])
|
||||
const nonMixItem = ref<PrescriptionItem>(genPrescriptionItem())
|
||||
|
||||
mixItem.value.prescription_id = id
|
||||
nonMixItem.value.prescription_id = id
|
||||
|
||||
const { backToList } = useQueryCRUDMode()
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Pembuatan Order Obat / Resep',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
const mixDialogOpenStatus = ref(false)
|
||||
const nonMixDialogOpenStatus = ref(false)
|
||||
const medicines = ref<Medicine[]>([])
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
let item: PrescriptionItem | null
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
item = pickItem()
|
||||
if (item) {
|
||||
isItemDetailDialogOpen.value = true
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
item = pickItem()
|
||||
if (item) {
|
||||
if(item.isMix) {
|
||||
mixDialogOpenStatus.value = true
|
||||
} else {
|
||||
nonMixDialogOpenStatus.value = true
|
||||
}
|
||||
getMedicines('', item.medicine_code)
|
||||
}
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getPrescriptionItems()
|
||||
})
|
||||
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
} else if (type === 'delete') {
|
||||
isDeleteConfirmationOpen.value = true
|
||||
} else if (type === 'submit') {
|
||||
isSubmitConfirmationOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function removePrescription() {
|
||||
const res = await remove(id)
|
||||
if (res.success) {
|
||||
backToList()
|
||||
}
|
||||
// handleActionRemove(recId.value, getItems, toast)
|
||||
}
|
||||
|
||||
async function submitPrescription() {
|
||||
const res = await submit(id)
|
||||
if (res.success) {
|
||||
backToList()
|
||||
}
|
||||
// handleActionRemove(recId.value, getItems, toast)
|
||||
}
|
||||
|
||||
function addItem(mode: 'mix' | 'non-mix') {
|
||||
if (mode === 'mix') {
|
||||
mixDialogOpenStatus.value = true
|
||||
} else if (mode === 'non-mix') {
|
||||
nonMixDialogOpenStatus.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMix() {
|
||||
const res = await createItem(mixItem.value)
|
||||
if (res.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
getPrescriptionItems()
|
||||
mixDialogOpenStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNonMix() {
|
||||
let res: any;
|
||||
if(!nonMixItem.value.id) {
|
||||
res = await createItem(nonMixItem.value)
|
||||
} else {
|
||||
res = await updateItem(nonMixItem.value.id, nonMixItem.value)
|
||||
}
|
||||
if (res.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
getPrescriptionItems()
|
||||
nonMixDialogOpenStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getMedicines(value: string, code?: string) {
|
||||
const res = await getMedicineList({ 'search': value, 'includes': 'medicineForm', 'code': code })
|
||||
if (res.success) {
|
||||
medicines.value = res.body.data
|
||||
} else {
|
||||
medicines.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function pickItem(): PrescriptionItem | null {
|
||||
const item = items.value.find(item => item.id === recId.value)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
if(item.isMix) {
|
||||
mixItem.value = item
|
||||
} else {
|
||||
nonMixItem.value = item
|
||||
}
|
||||
return item
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<Entry :data="data" />
|
||||
|
||||
<!-- <div class="mb-8">
|
||||
<Button>
|
||||
<Icon name="i-lucide-check" />
|
||||
Simpan
|
||||
</Button>
|
||||
</div> -->
|
||||
|
||||
<ItemListEntry
|
||||
:data="items"
|
||||
@add="addItem"/>
|
||||
|
||||
<Separator class="my-5" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<Nav @click="navClick" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm delete -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isDeleteConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="removePrescription()"
|
||||
/>
|
||||
|
||||
<!-- Confirm submit -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
action="delete"
|
||||
customTitle="Ajukan Resep"
|
||||
customMessage="Proses akan mengajukan resep ini untuk diproses lebih lanjut. Lanjutkan?"
|
||||
customConfirmText="Ajukan"
|
||||
:record="recItem"
|
||||
@confirm="submitPrescription"
|
||||
@cancel=""
|
||||
/>
|
||||
|
||||
<!-- Mix form -->
|
||||
<Dialog
|
||||
v-model:open="mixDialogOpenStatus"
|
||||
title="Pembuatan Racikan"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<MixItemEntry
|
||||
:data="mixItem"
|
||||
:items="medicinemixItems"
|
||||
:medicines="medicines"
|
||||
@close="mixDialogOpenStatus = false"
|
||||
@save="saveMix"
|
||||
@update:searchText="getMedicines"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Non mix form -->
|
||||
<Dialog
|
||||
v-model:open="nonMixDialogOpenStatus"
|
||||
title="Pembuatan Non Racikan"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<NonMixItemEntry
|
||||
:data="nonMixItem"
|
||||
:medicines="medicines"
|
||||
@close="mixDialogOpenStatus = false"
|
||||
@save="saveNonMix"
|
||||
@update:searchText="getMedicines"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Detail item -->
|
||||
<Dialog
|
||||
v-model:open="isItemDetailDialogOpen"
|
||||
title="Detail Obat"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
>
|
||||
<DE.Block mode="preview" label-size="small">
|
||||
<DE.Cell>
|
||||
<DE.Label>Nama</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.medicine.name }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label>Dosis</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.dose }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label>Sediaan</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.medicine.medicineForm.name }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label>Jumlah</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.quantity }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
<DE.Cell>
|
||||
<DE.Label>Cara Pakai</DE.Label>
|
||||
<DE.Colon />
|
||||
<DE.Field>
|
||||
{{ recItem.Usage }}
|
||||
</DE.Field>
|
||||
</DE.Cell>
|
||||
</DE.Block>
|
||||
</Dialog>
|
||||
|
||||
<!-- Confirm delete items -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getPrescriptionItems, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -1,37 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
// Composables
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
|
||||
// Pubs component
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import PrescriptionItemListEntry from '~/components/app/prescription-item/list-entry.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
const data = ref([])
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionRemove,
|
||||
handleActionSave,
|
||||
} from '~/handlers/prescription.handler'
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (_val: string) => {
|
||||
// filter patient list
|
||||
},
|
||||
onClear: () => {
|
||||
// clear url param
|
||||
},
|
||||
}
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/prescription.service'
|
||||
import FlatList from '~/components/app/prescription/flat-list.vue'
|
||||
import Grouped from '~/components/app/prescription/grouped-list.vue'
|
||||
import type { Prescription } from '~/models/prescription'
|
||||
import { submit } from '~/services/prescription.service'
|
||||
import type { ToastFn } from '~/handlers/_handler'
|
||||
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
const route = useRoute()
|
||||
const { setQueryParams } = useQueryParam()
|
||||
|
||||
const title = ref('')
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
|
||||
const plainEid = route.params.id
|
||||
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList<Prescription>({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
'encounter-id': encounter_id,
|
||||
includes: 'doctor,doctor-employee,doctor-employee-person,items,items-medicine,items-medicine-medicineForm',
|
||||
search: params.search,
|
||||
page: params.page,
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'prescription'
|
||||
})
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
function updateProvidedVal(val: boolean) {
|
||||
flatMode.value = val
|
||||
}
|
||||
const flatMode = ref(false)
|
||||
const flatModeLabel = ref('Mode Flat: Tidak')
|
||||
provide('flatMode', { flatMode, updateProvidedVal })
|
||||
watch(flatMode, (newVal) => {
|
||||
flatModeLabel.value = newVal ? 'Mode Flat: Ya' : 'Mode Flat: Tidak'
|
||||
})
|
||||
|
||||
const vFn = () => {} // Void function
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Resep Obat',
|
||||
icon: 'i-lucide-panel-bottom',
|
||||
title: 'Order Obat',
|
||||
icon: 'i-lucide-box',
|
||||
components: [
|
||||
{
|
||||
component: defineAsyncComponent(() => import('~/components/pub/my-ui/toggle/provided-toggle.vue')),
|
||||
props: { variant: 'outline', label: flatModeLabel, providedKey: 'flatMode' }
|
||||
},
|
||||
],
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => navigateTo('/tools-equipment-src/equipment/add'),
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: async () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isReadonly.value = false
|
||||
const saveResp = await handleActionSave({ encounter_id }, vFn, vFn, vFn)
|
||||
if (saveResp.success) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': saveResp.body?.data?.id.toString()
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,33 +111,98 @@ provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
onMounted(() => {
|
||||
getMaterialList()
|
||||
})
|
||||
function confirmCancel(data: Prescription) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
async function getMaterialList() {
|
||||
isLoading.dataListLoading = true
|
||||
function goToEdit(data: Prescription) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': data.id.toString()
|
||||
})
|
||||
recItem.value = data
|
||||
}
|
||||
|
||||
// const resp = await xfetch('/api/v1/material')
|
||||
// if (resp.success) {
|
||||
// data.value = (resp.body as Record<string, any>).data
|
||||
// }
|
||||
function confirmSubmit(data: Prescription) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isSubmitConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
isLoading.dataListLoading = false
|
||||
async function handleActionSubmit(id: number, refresh: () => void, toast: ToastFn) {
|
||||
const result = await submit(id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
setTimeout(refresh, 300)
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: 'Gagal menjalankan perintah', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
<template v-if="!flatMode">
|
||||
<Grouped
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="confirmCancel"
|
||||
@edit="goToEdit"
|
||||
@submit="confirmSubmit"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<FlatList
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="confirmCancel"
|
||||
@edit="goToEdit"
|
||||
@submit="confirmSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<AppPrescriptionList v-if="!isLoading.dataListLoading" />
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<div class="flex mb-2">
|
||||
<div class="w-20">Tanggal</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.createdAt.substring(0, 10) }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="w-20">DPJP</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.doctor?.employee?.person?.name }}</div>
|
||||
</div>
|
||||
</RecordConfirmation>
|
||||
|
||||
<AppPrescriptionEntry />
|
||||
|
||||
<PrescriptionItemListEntry :data=[] />
|
||||
<div>
|
||||
<Button>
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
action="delete"
|
||||
customTitle="Ajukan Resep"
|
||||
customMessage="Proses akan mengajukan resep ini untuk diproses lebih lanjut. Lanjutkan?"
|
||||
customConfirmText="Ajukan"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionSubmit(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<div class="flex mb-2">
|
||||
<div class="w-20">Tanggal</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.createdAt.substring(0, 10) }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="w-20">DPJP</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.doctor?.employee?.person?.name }}</div>
|
||||
</div>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import * as CH from '~/components/pub/my-ui/content-header'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import NavEntry from '~/components/pub/my-ui/nav-footer/ba-de-dr-su.vue'
|
||||
import NavDetail from '~/components/pub/my-ui/nav-footer/ba.vue'
|
||||
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
|
||||
|
||||
// Procedure Room
|
||||
import type { ProcedureRoom } from '~/models/procedure-room'
|
||||
import { getList as getProcedureRoomList } from '~/services/procedure-room.service'
|
||||
import PRSwitcher from '~/components/app/procedure-room/switcher.vue'
|
||||
import PRMultiOptPicker from '~/components/app/procedure-room/multi-opt-picker.vue'
|
||||
import PRPicker from '~/components/app/procedure-room/picker.vue'
|
||||
|
||||
// Material Package
|
||||
import type { MaterialPackage } from '~/models/material-package'
|
||||
import { getList as getMaterialPackageList } from '~/services/material-package.service'
|
||||
|
||||
// Material Package Item
|
||||
import type { MaterialPackageItem } from '~/models/material-package-item'
|
||||
import { getList as getmaterialPackageItems } from '~/services/material-package-item.service'
|
||||
import MPSwitcher from '~/components/app/material-package/switcher.vue'
|
||||
import MPIQuickList from '~/components/app/material-package-item/quick-list.vue'
|
||||
|
||||
// Main data
|
||||
import { getDetail } from '~/services/procedure-room-order.service'
|
||||
import Detail from '~/components/app/procedure-room-order/detail.vue'
|
||||
|
||||
// Items data
|
||||
import type { ProcedureRoomOrderItem } from '~/models/procedure-room-order-item'
|
||||
import {
|
||||
getList as getOrderItemList,
|
||||
create as createOrderItem,
|
||||
remove as removeOrderItem,
|
||||
} from '~/services/procedure-room-order-item.service'
|
||||
import ItemListEntry from '~/components/app/procedure-room-order-item/list-entry.vue'
|
||||
import ItemListDetail from '~/components/app/procedure-room-order-item/list-detail.vue'
|
||||
|
||||
// data
|
||||
const { backToList, crudQueryParams } = useQueryCRUD()
|
||||
const id = crudQueryParams.value.recordId
|
||||
const dataRes = await getDetail(
|
||||
typeof id === 'string' ? parseInt(id) : 0,
|
||||
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
|
||||
)
|
||||
const data = dataRes.body?.data
|
||||
const items = ref<ProcedureRoomOrderItem[]>([])
|
||||
|
||||
// Header
|
||||
const headerConfig: CH.Config = {
|
||||
title: 'Order Ruang Tindakan',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
//
|
||||
const pickerDialogOpen = ref(false)
|
||||
const procedureRooms = ref<ProcedureRoom[]>([])
|
||||
const procedureRoomType = ref('procedure')
|
||||
getProcedureRooms(procedureRoomType.value)
|
||||
watch(procedureRoomType, async (newValue) => {
|
||||
getProcedureRooms(newValue)
|
||||
})
|
||||
|
||||
//
|
||||
const materialPackages = ref<MaterialPackage[]>([])
|
||||
const selectedMaterialPackage = ref('')
|
||||
const res = await getMaterialPackageList()
|
||||
if (res.success) {
|
||||
materialPackages.value = res.body.data
|
||||
}
|
||||
|
||||
//
|
||||
const materialPackageItems = ref<MaterialPackageItem[]>([])
|
||||
watch(selectedMaterialPackage, async (newValue) => {
|
||||
const res = await getmaterialPackageItems({
|
||||
'material-package-code': selectedMaterialPackage.value,
|
||||
includes: 'material'
|
||||
})
|
||||
if (res.success) {
|
||||
materialPackageItems.value = res.body.data
|
||||
}
|
||||
})
|
||||
|
||||
// last flow
|
||||
onMounted(async () => {
|
||||
await getItems()
|
||||
})
|
||||
|
||||
///// functions
|
||||
async function getProcedureRooms(typeCode: string) {
|
||||
const res = await getProcedureRoomList({ 'type-code': typeCode, includes: 'infra' })
|
||||
if (res.success) {
|
||||
procedureRooms.value = res.body.data
|
||||
}
|
||||
}
|
||||
|
||||
async function getItems() {
|
||||
const res = await getOrderItemList({
|
||||
'procedure-room-order-id': crudQueryParams.value.recordId,
|
||||
includes: 'procedureRoom,procedureRoom-infra',
|
||||
})
|
||||
if (res.success) {
|
||||
items.value = res.body.data
|
||||
}
|
||||
}
|
||||
|
||||
async function pickItem(item: ProcedureRoom) {
|
||||
const exItem = items.value.find(e => e.procedureRoom_code === item.code)
|
||||
if (exItem) {
|
||||
await removeOrderItem(exItem.id)
|
||||
await getItems()
|
||||
} else {
|
||||
const intId = parseInt(id?.toString() || '0')
|
||||
await createOrderItem({
|
||||
procedureRoomOrder_id: intId,
|
||||
procedureRoom_code: item.code,
|
||||
})
|
||||
await getItems()
|
||||
}
|
||||
}
|
||||
|
||||
function requestItem() {
|
||||
pickerDialogOpen.value = true
|
||||
}
|
||||
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CH.ContentHeader v-bind="headerConfig" />
|
||||
|
||||
<Separator class="mb-4" />
|
||||
|
||||
<Detail :data="data" />
|
||||
|
||||
<template v-if="data.status_code == 'new'">
|
||||
<ItemListEntry :data="items" @requestItem="requestItem" />
|
||||
<MPSwitcher :data="materialPackages" v-model="selectedMaterialPackage" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ItemListDetail :data="items" @requestItem="requestItem" />
|
||||
</template>
|
||||
<MPIQuickList :data="materialPackageItems" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<NavEntry v-if="data.status_code == 'new'" @click="navClick" />
|
||||
<NavDetail v-else @click="navClick" />
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:open="pickerDialogOpen"
|
||||
title="Pilih Item"
|
||||
size="2xl"
|
||||
prevent-outside
|
||||
>
|
||||
<PRSwitcher v-model="procedureRoomType" />
|
||||
<PRPicker
|
||||
:data="procedureRooms"
|
||||
:pick-mode="procedureRoomType == 'procedure' ? 'multi' : 'single'"
|
||||
v-model="items"
|
||||
@pick="pickItem"
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
// Composables
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
|
||||
// Handlers
|
||||
import type { ToastFn } from '~/handlers/_handler'
|
||||
|
||||
// Pubs component
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Order
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/procedure-room-order.handler'
|
||||
import { getList, create, submit } from '~/services/procedure-room-order.service'
|
||||
import type { ProcedureRoomOrder } from '~/models/procedure-room-order'
|
||||
import List from '~/components/app/procedure-room-order/list.vue'
|
||||
|
||||
// Common prep
|
||||
const route = useRoute()
|
||||
const { setQueryParams } = useQueryParam()
|
||||
const { crudQueryParams } = useQueryCRUD()
|
||||
|
||||
const plainEid = route.params.id
|
||||
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0
|
||||
const isSubmitConfirmationOpen = ref(false)
|
||||
|
||||
// Main data
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList<ProcedureRoomOrder>({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
'encounter-id': encounter_id,
|
||||
includes: 'items',
|
||||
search: params.search,
|
||||
page: params.page,
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'prescription'
|
||||
})
|
||||
|
||||
// Header things
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Order Ruang Tindakan',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: async () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
const res = await create({ encounter_id })
|
||||
if (res.success) {
|
||||
crudQueryParams.value = { mode: 'entry', recordId: res.body?.data.id.toString() || '0' }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// list actions
|
||||
const timestamp = ref<any>({})
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('timestamp', timestamp)
|
||||
watch(recAction, () => {
|
||||
if (!recAction.value) {
|
||||
return
|
||||
} else if (recAction.value === ActionEvents.showDetail) {
|
||||
crudQueryParams.value = { mode: 'entry', recordId: recId.value || '0' }
|
||||
} else if (recAction.value === ActionEvents.showConfirmSubmit) {
|
||||
isSubmitConfirmationOpen.value = true
|
||||
} else if (recAction.value === ActionEvents.showConfirmDelete) {
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
recAction.value = ''
|
||||
})
|
||||
|
||||
///// functions
|
||||
|
||||
function confirmCancel(data: ProcedureRoomOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
function goToEdit(data: ProcedureRoomOrder) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': data.id.toString()
|
||||
})
|
||||
recItem.value = data
|
||||
}
|
||||
|
||||
function confirmSubmit(data: ProcedureRoomOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isSubmitConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
async function handleActionSubmit(id: number, refresh: () => void, toast: ToastFn) {
|
||||
const result = await submit(id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Resep telah di ajukan', variant: 'default' })
|
||||
setTimeout(refresh, 300)
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: 'Gagal menjalankan perintah', variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<List
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="confirmCancel"
|
||||
@edit="goToEdit"
|
||||
@submit="confirmSubmit"
|
||||
/>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<div class="flex mb-2">
|
||||
<div class="w-20">Tanggal</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.createdAt?.substring(0, 10) }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="w-20">DPJP</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.doctor?.employee?.person?.name }}</div>
|
||||
</div>
|
||||
</RecordConfirmation>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isSubmitConfirmationOpen"
|
||||
action="delete"
|
||||
customTitle="Ajukan Resep"
|
||||
customMessage="Proses akan mengajukan resep ini untuk diproses lebih lanjut. Lanjutkan?"
|
||||
customConfirmText="Ajukan"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionSubmit(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<div class="flex mb-2">
|
||||
<div class="w-20">Tanggal</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.createdAt.substring(0, 10) }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="w-20">DPJP</div>
|
||||
<div class="w-4">:</div>
|
||||
<div class="">{{ recItem.doctor?.employee?.person?.name }}</div>
|
||||
</div>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
<div class="hidden">{{ crudQueryParams.mode }}</div>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
|
||||
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
|
||||
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
|
||||
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// mcu src category
|
||||
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
|
||||
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
|
||||
|
||||
// mcu src
|
||||
import { type McuSrc } from '~/models/mcu-src'
|
||||
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
|
||||
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
|
||||
|
||||
// mcu order
|
||||
import { getDetail } from '~/services/mcu-order.service'
|
||||
import Detail from '~/components/app/mcu-order/detail.vue'
|
||||
|
||||
// mcu order item, manually not using composable
|
||||
import {
|
||||
getList as getMcuOrderItemList,
|
||||
create as createMcuOrderItem,
|
||||
remove as removeMcuOrderItem,
|
||||
} from '~/services/mcu-order-item.service'
|
||||
import { type McuOrderItem } from '~/models/mcu-order-item'
|
||||
import ItemListEntry from '~/components/app/mcu-order-item/list-entry.vue'
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
// declaration & flows
|
||||
|
||||
// MCU Order
|
||||
const { getQueryParam } = useQueryParam()
|
||||
const id = getQueryParam('id')
|
||||
const dataRes = await getDetail(
|
||||
typeof id === 'string' ? parseInt(id) : 0,
|
||||
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
|
||||
)
|
||||
const data = dataRes.body?.data
|
||||
|
||||
// MCU items
|
||||
const items = ref<McuOrderItem[]>([])
|
||||
|
||||
// MCU Categories
|
||||
const mcuSrcCategoryRes = await getMcuCategoryList()
|
||||
const mcuSrcCategories = mcuSrcCategoryRes.body?.data
|
||||
const selectedMcuSrcCategory_code = ref('')
|
||||
|
||||
// MCU Sources
|
||||
const mcuSrcs = ref<McuSrc[]>([])
|
||||
|
||||
// const {
|
||||
// data: items,
|
||||
// fetchData: getItems,
|
||||
// } = usePaginatedList<McuOrderItem> ({
|
||||
// fetchFn: async ({ page, search }) => {
|
||||
// const result = await getMcuOrderItemList({ 'mcu-order-id': id, search, page })
|
||||
// if (result.success) {
|
||||
// items.value = result.body.data
|
||||
// }
|
||||
// return { success: result.success || false, body: result.body || {} }
|
||||
// },
|
||||
// entityName: 'mcu-order-item',
|
||||
// })
|
||||
|
||||
const { backToList } = useQueryCRUDMode()
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail dan List Item Order Radiologi ',
|
||||
icon: 'i-lucide-box',
|
||||
}
|
||||
|
||||
const pickerDialogOpen = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await getItems()
|
||||
})
|
||||
|
||||
watch(selectedMcuSrcCategory_code, async () => {
|
||||
const res = await getMcuSrcList({ 'mcu-src-category-code': selectedMcuSrcCategory_code.value })
|
||||
mcuSrcs.value = res.body?.data
|
||||
})
|
||||
|
||||
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
}
|
||||
}
|
||||
|
||||
function requestItem() {
|
||||
pickerDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function pickItem(item: McuSrc) {
|
||||
const exItem = items.value.find(e => e.mcuSrc_id === item.id)
|
||||
if (exItem) {
|
||||
await removeMcuOrderItem(exItem.id)
|
||||
await getItems()
|
||||
} else {
|
||||
const intId = parseInt(id?.toString() || '0')
|
||||
await createMcuOrderItem({
|
||||
mcuOrder_id: intId,
|
||||
mcuSrc_id: item.id,
|
||||
})
|
||||
await getItems()
|
||||
}
|
||||
}
|
||||
|
||||
async function getItems() {
|
||||
const itemsRes = await getMcuOrderItemList({ 'mcu-order-id': id, includes: 'mcuSrc,mcuSrc-mcuSrcCategory' })
|
||||
if (itemsRes.success) {
|
||||
items.value = itemsRes.body.data
|
||||
} else {
|
||||
items.value = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<Detail :data="data" />
|
||||
|
||||
<ItemListEntry
|
||||
:data="items"
|
||||
@requestItem="requestItem"/>
|
||||
<Separator class="my-5" />
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<Nav @click="navClick" />
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:open="pickerDialogOpen"
|
||||
title="Pilih Item"
|
||||
size="2xl"
|
||||
prevent-outside
|
||||
>
|
||||
<ScrCategorySwitcher :data="mcuSrcCategories" v-model="selectedMcuSrcCategory_code" />
|
||||
<McuSrcPicker v-model="items" :data-source="mcuSrcs" @pick="pickItem" />
|
||||
<Separator />
|
||||
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isRecordConfirmationOpen,
|
||||
handleActionSave,
|
||||
handleActionRemove,
|
||||
} from '~/handlers/mcu-order.handler'
|
||||
|
||||
// Apps
|
||||
import { getList, getDetail } from '~/services/mcu-order.service'
|
||||
import List from '~/components/app/mcu-order/list.vue'
|
||||
import type { McuOrder } from '~/models/mcu-order'
|
||||
|
||||
const route = useRoute()
|
||||
const { setQueryParams } = useQueryParam()
|
||||
|
||||
const title = ref('')
|
||||
const addTrigger = ref(false)
|
||||
|
||||
const plainEid = route.params.id
|
||||
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList<McuOrder>({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
search,
|
||||
page,
|
||||
'scope-code': "rad",
|
||||
'encounter-id': encounter_id,
|
||||
includes: 'doctor,doctor-employee,doctor-employee-person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'mcu-order'
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Order Radiologi',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
addTrigger.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
addTrigger.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Detail Order Radiologi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getMyDetail(recId.value)
|
||||
title.value = 'Edit Order Radiologi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
watch([addTrigger], async () => {
|
||||
if (addTrigger.value) {
|
||||
addTrigger.value = false;
|
||||
const saveResp = await handleActionSave({ encounter_id, "scope_code": "rad" }, getMyList, () =>{}, toast)
|
||||
if (saveResp.success) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': saveResp.body?.data?.id.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
})
|
||||
|
||||
function cancel(data: McuOrder) {
|
||||
recId.value = data.id
|
||||
recItem.value = data
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
function edit(data: McuOrder) {
|
||||
setQueryParams({
|
||||
'mode': 'entry',
|
||||
'id': data.id.toString()
|
||||
})
|
||||
recItem.value = data
|
||||
}
|
||||
|
||||
function submit(data: McuOrder) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<List
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@cancel="cancel"
|
||||
@edit="edit"
|
||||
@submit="submit"
|
||||
/>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
//
|
||||
import List from './list.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { mode } = useQueryCRUDMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
|
||||
<Entry v-else :encounter_id="encounter_id" />
|
||||
</template>
|
||||
@@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import type { ExposedForm } from '~/types/form';
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { ResumeSchema } from '~/schemas/resume.schema';
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date';
|
||||
import type { DateRange } from 'radix-vue';
|
||||
import { getPatients } from '~/services/patient.service';
|
||||
import ActionHistoryDialog from '~/components/app/resume/history-list/action-history-dialog.vue';
|
||||
import ConsultationHistoryDialog from '~/components/app/resume/history-list/consultation-history-dialog.vue';
|
||||
import SupportingHistoryDialog from '~/components/app/resume/history-list/supporting-history-dialog.vue';
|
||||
import FarmacyHistoryDialog from '~/components/app/resume/history-list/farmacy-history-dialog.vue';
|
||||
import NationalProgramHistoryDialog from '~/components/app/resume/history-list/national-program-history-dialog.vue';
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const personPatientForm = ref<ExposedForm<any> | null>(null)
|
||||
const actionHistoryData = usePaginatedList({
|
||||
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'patient',
|
||||
})
|
||||
const consultationHistoryData = usePaginatedList({
|
||||
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'patient',
|
||||
})
|
||||
const supportingHistoryData = usePaginatedList({
|
||||
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'patient',
|
||||
})
|
||||
const farmacyHistoryData = usePaginatedList({
|
||||
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'patient',
|
||||
})
|
||||
const nationalProgramServiceHistoryData = usePaginatedList({
|
||||
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
|
||||
entityName: 'patient',
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
const isConfirmationOpen = ref(false)
|
||||
const isActionHistoryOpen = ref<boolean>(false)
|
||||
const isConsultationHistoryOpen = ref<boolean>(false)
|
||||
const isSupportingHistoryOpen = ref<boolean>(false)
|
||||
const isFarmacyHistoryOpen = ref<boolean>(false)
|
||||
const isNationalProgramServiceHistoryOpen = ref<boolean>(false)
|
||||
|
||||
provide(`isActionHistoryOpen`, isActionHistoryOpen)
|
||||
provide(`isConsultationHistoryOpen`, isConsultationHistoryOpen)
|
||||
provide(`isSupportingHistoryOpen`, isSupportingHistoryOpen)
|
||||
provide(`isFarmacyHistoryOpen`, isFarmacyHistoryOpen)
|
||||
provide(`isNationalProgramServiceHistoryOpen`, isNationalProgramServiceHistoryOpen)
|
||||
|
||||
const defaultDate = {
|
||||
start: new CalendarDate(2022, 1, 20),
|
||||
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||
}
|
||||
|
||||
const actionHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||
const consultationHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||
const supportingHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||
const farmacyHistoryDateValue = ref(defaultDate) as Ref<DateRange>
|
||||
const nationalProgramServiceSearch = ref<string>('')
|
||||
const nationalProgramServiceSelectedStatus = ref<string>('all')
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
// #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 === 'back') {
|
||||
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 Resume</div>
|
||||
<AppResumeAdd
|
||||
ref="personPatientForm"
|
||||
:schema="ResumeSchema"
|
||||
:resume-arrangement-type="personPatientForm?.values.arrangement"/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
<ActionHistoryDialog
|
||||
v-model:is-modal-open="isActionHistoryOpen"
|
||||
v-model:date-value="actionHistoryDateValue"
|
||||
:data="actionHistoryData.data.value"
|
||||
:pagination-meta="actionHistoryData.paginationMeta"
|
||||
@page-change="actionHistoryData.handlePageChange"
|
||||
/>
|
||||
|
||||
<p v-if="isConsultationHistoryOpen === true">aaaaaaaaaaaaaaa</p>
|
||||
<ConsultationHistoryDialog
|
||||
v-model:is-modal-open="isConsultationHistoryOpen"
|
||||
v-model:date-value="consultationHistoryDateValue"
|
||||
:data="consultationHistoryData.data.value"
|
||||
:pagination-meta="consultationHistoryData.paginationMeta"
|
||||
@page-change="consultationHistoryData.handlePageChange"
|
||||
/>
|
||||
|
||||
<SupportingHistoryDialog
|
||||
v-model:is-modal-open="isSupportingHistoryOpen"
|
||||
v-model:date-value="supportingHistoryDateValue"
|
||||
:data="supportingHistoryData.data.value"
|
||||
:pagination-meta="supportingHistoryData.paginationMeta"
|
||||
@page-change="supportingHistoryData.handlePageChange"
|
||||
/>
|
||||
|
||||
<FarmacyHistoryDialog
|
||||
v-model:is-modal-open="isFarmacyHistoryOpen"
|
||||
v-model:date-value="farmacyHistoryDateValue"
|
||||
:data="farmacyHistoryData.data.value"
|
||||
:pagination-meta="farmacyHistoryData.paginationMeta"
|
||||
@page-change="farmacyHistoryData.handlePageChange"
|
||||
/>
|
||||
|
||||
<NationalProgramHistoryDialog
|
||||
v-model:is-modal-open="isNationalProgramServiceHistoryOpen"
|
||||
v-model:search-value="nationalProgramServiceSearch"
|
||||
v-model:status-value="nationalProgramServiceSelectedStatus"
|
||||
:data="nationalProgramServiceHistoryData.data.value"
|
||||
:pagination-meta="nationalProgramServiceHistoryData.paginationMeta"
|
||||
@page-change="nationalProgramServiceHistoryData.handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,228 @@
|
||||
<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 Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
|
||||
import { getPatients, removePatient } from '~/services/patient.service'
|
||||
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import { VerificationSchema } from '~/schemas/verification.schema'
|
||||
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
|
||||
import type { RoleAccesses } from '~/models/role'
|
||||
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
|
||||
import { unauthorizedToast } from '~/lib/utils'
|
||||
import { permissions } from '~/const/page-permission/ambulatory'
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region Permission
|
||||
const roleAccess: RoleAccesses = permissions['/ambulatory/encounter'] || {}
|
||||
const { getPagePermissions } = useRBAC()
|
||||
const pagePermission = getPagePermissions(roleAccess)
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
}>(), {
|
||||
redirectToForm: () => { }
|
||||
})
|
||||
|
||||
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 verificationInputForm = ref<ExposedForm<any> | null>(null)
|
||||
const isVerifyDialogOpen = ref(false)
|
||||
const isDocPreviewDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const isCaptchaValid = ref(false)
|
||||
provide('isCaptchaValid', isCaptchaValid)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Resume",
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
if (pagePermission.canCreate) {
|
||||
headerPrep.addNav = {
|
||||
label: "Resume",
|
||||
onClick: () => {
|
||||
props.redirectToForm()
|
||||
},
|
||||
}
|
||||
}
|
||||
// #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
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
// 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 === 'back') {
|
||||
isVerifyDialogOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmDelete() {
|
||||
try {
|
||||
const result = await removePatient(recId.value)
|
||||
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.showVerify:
|
||||
isVerifyDialogOpen.value = true
|
||||
if(pagePermission.canUpdate) {
|
||||
isVerifyDialogOpen.value = true
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
case ActionEvents.showValidate:
|
||||
isRecordConfirmationOpen.value = true
|
||||
if(pagePermission.canUpdate) {
|
||||
isRecordConfirmationOpen.value = true
|
||||
} else {
|
||||
unauthorizedToast()
|
||||
}
|
||||
break
|
||||
case ActionEvents.showPrint:
|
||||
isDocPreviewDialogOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
|
||||
<!-- <AppTherapyProtocolList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"/> -->
|
||||
<AppResumeList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"/>
|
||||
|
||||
<Dialog v-model:open="isVerifyDialogOpen" title="Verifikasi">
|
||||
<AppResumeVerifyDialog
|
||||
ref="verificationInputForm"
|
||||
:schema="VerificationSchema" />
|
||||
<div class="flex justify-end">
|
||||
<Action v-show="isCaptchaValid" :enable-draft="false" @click="handleActionClick" />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
|
||||
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
|
||||
</Dialog>
|
||||
|
||||
<Confirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
title="Validasi Data"
|
||||
message="Apakah Anda yakin ingin menvalidasi data ini?"
|
||||
confirm-text="Validasi"
|
||||
@confirm="handleConfirmDelete"
|
||||
@cancel="handleCancelConfirmation"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Entry from './add.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list"
|
||||
:encounter_id="encounter_id"
|
||||
:redirectToForm="goToEntry"
|
||||
:redirectToDetail="goToDetail"/>
|
||||
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
|
||||
</template>
|
||||
@@ -1,113 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
const openPatient = ref(false)
|
||||
const openLetter = ref(false)
|
||||
const openHistory = ref(false)
|
||||
const selectedPatient = ref('3456512345678880')
|
||||
const selectedLetter = ref('SK22334442')
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const patients = [
|
||||
{
|
||||
ktp: '3456512345678880',
|
||||
rm: 'RM23311224',
|
||||
bpjs: '334423213214',
|
||||
nama: 'Ahmad Baidowi',
|
||||
},
|
||||
{
|
||||
ktp: '345678804565123',
|
||||
rm: 'RM23455667',
|
||||
bpjs: '33442367656',
|
||||
nama: 'Bian Maulana',
|
||||
},
|
||||
]
|
||||
// Components
|
||||
import AppSepEntryForm from '~/components/app/sep/entry-form.vue'
|
||||
import AppViewPatient from '~/components/app/patient/view-patient.vue'
|
||||
import AppViewHistory from '~/components/app/sep/view-history.vue'
|
||||
import AppViewLetter from '~/components/app/sep/view-letter.vue'
|
||||
|
||||
const letters = [
|
||||
{
|
||||
noSurat: 'SK22334442',
|
||||
tglRencana: '12 Agustus 2025',
|
||||
noSep: 'SEP3232332',
|
||||
namaPasien: 'Ahmad Baidowi',
|
||||
noBpjs: '33442331214',
|
||||
klinik: 'Penyakit Dalam',
|
||||
dokter: 'dr. Andi Prasetyo, Sp.PD-KHOM',
|
||||
},
|
||||
{
|
||||
noSurat: 'SK99120039',
|
||||
tglRencana: '12 Agustus 2025',
|
||||
noSep: 'SEP4443232',
|
||||
namaPasien: 'Bian Maulana',
|
||||
noBpjs: '33442367656',
|
||||
klinik: 'Gigi',
|
||||
dokter: 'dr. Achmad Suparjo',
|
||||
},
|
||||
]
|
||||
// Handler
|
||||
import { useIntegrationSepEntry } from '~/handlers/integration-sep-entry.handler'
|
||||
import { useIntegrationSepDetail } from '~/handlers/integration-sep-detail.handler'
|
||||
|
||||
const histories = [
|
||||
{
|
||||
no_sep: "SP23311224",
|
||||
tgl_sep: "12 Agustus 2025",
|
||||
no_rujukan: "123444",
|
||||
diagnosis: "C34.9 – Karsinoma Paru",
|
||||
pelayanan: "Rawat Jalan",
|
||||
kelas: "Kelas II",
|
||||
},
|
||||
{
|
||||
no_sep: "SP23455667",
|
||||
tgl_sep: "11 Agustus 2025",
|
||||
no_rujukan: "2331221",
|
||||
diagnosis: "K35 – Apendisitis akut",
|
||||
pelayanan: "Rawat Jalan",
|
||||
kelas: "Kelas II",
|
||||
},
|
||||
]
|
||||
const {
|
||||
histories,
|
||||
letters,
|
||||
patients,
|
||||
doctors,
|
||||
diagnoses,
|
||||
facilitiesFrom,
|
||||
facilitiesTo,
|
||||
supportCodesList,
|
||||
serviceTypesList,
|
||||
registerMethodsList,
|
||||
accidentsList,
|
||||
purposeOfVisitsList,
|
||||
proceduresList,
|
||||
assessmentsList,
|
||||
provincesList,
|
||||
citiesList,
|
||||
districtsList,
|
||||
classLevelsList,
|
||||
classLevelUpgradesList,
|
||||
classPaySourcesList,
|
||||
isServiceHidden,
|
||||
isSaveLoading,
|
||||
isLetterReadonly,
|
||||
isLoadingPatient,
|
||||
openPatient,
|
||||
openLetter,
|
||||
openHistory,
|
||||
selectedLetter,
|
||||
selectedObjects,
|
||||
selectedServiceType,
|
||||
selectedAdmissionType,
|
||||
specialistsTree,
|
||||
selectedPatient,
|
||||
paginationMeta,
|
||||
getLetterMappers,
|
||||
getPatientsList,
|
||||
getPatientByIdentifierSearch,
|
||||
handleSaveLetter,
|
||||
handleSavePatient,
|
||||
handleEvent,
|
||||
handleFetch,
|
||||
handleInit,
|
||||
} = useIntegrationSepEntry()
|
||||
|
||||
function handleSavePatient() {
|
||||
console.log('Pasien dipilih:', selectedPatient.value)
|
||||
}
|
||||
const { valueObjects, getSepDetail } = useIntegrationSepDetail()
|
||||
|
||||
function handleSaveLetter() {
|
||||
console.log('Letter dipilih:', selectedLetter.value)
|
||||
}
|
||||
const props = defineProps<{
|
||||
mode: 'add' | 'edit' | 'detail' | 'link'
|
||||
}>()
|
||||
|
||||
function handleEvent(value: string) {
|
||||
if (value === 'search-patient') {
|
||||
openPatient.value = true
|
||||
return
|
||||
onMounted(async () => {
|
||||
await handleInit()
|
||||
if (['detail', 'link'].includes(props.mode)) {
|
||||
await getSepDetail()
|
||||
selectedObjects.value = { ...selectedObjects.value, ...valueObjects.value }
|
||||
}
|
||||
if (value === 'search-letter') {
|
||||
openLetter.value = true
|
||||
return
|
||||
}
|
||||
if (value === 'history-sep') {
|
||||
openHistory.value = true
|
||||
return
|
||||
}
|
||||
if (value === 'back') {
|
||||
navigateTo('/bpjs/sep')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
|
||||
<Icon name="i-lucide-panel-bottom" class="me-2" />
|
||||
<span class="font-semibold">Tambah</span> SEP
|
||||
<Icon
|
||||
name="i-lucide-panel-bottom"
|
||||
class="me-2"
|
||||
/>
|
||||
<span class="font-semibold">{{ ['detail', 'link'].includes(props.mode) ? 'Detail' : 'Tambah' }} SEP</span>
|
||||
</div>
|
||||
<AppSepEntryForm @event="handleEvent" />
|
||||
<AppSepTableSearchPatient
|
||||
<AppSepEntryForm
|
||||
:mode="props.mode"
|
||||
:is-save-loading="isSaveLoading"
|
||||
:is-service="isServiceHidden"
|
||||
:is-readonly="['detail', 'link'].includes(props.mode)"
|
||||
:doctors="doctors"
|
||||
:diagnoses="diagnoses"
|
||||
:facilities-from="facilitiesFrom"
|
||||
:facilities-to="facilitiesTo"
|
||||
:service-types="serviceTypesList"
|
||||
:register-methods="registerMethodsList"
|
||||
:accidents="accidentsList"
|
||||
:purposes="purposeOfVisitsList"
|
||||
:procedures="proceduresList"
|
||||
:assessments="assessmentsList"
|
||||
:support-codes="supportCodesList"
|
||||
:provinces="provincesList"
|
||||
:cities="citiesList"
|
||||
:districts="districtsList"
|
||||
:class-levels="classLevelsList"
|
||||
:class-level-upgrades="classLevelUpgradesList"
|
||||
:class-pay-sources="classPaySourcesList"
|
||||
:specialists="specialistsTree"
|
||||
:objects="selectedObjects"
|
||||
@fetch="handleFetch"
|
||||
@event="handleEvent"
|
||||
/>
|
||||
<AppViewPatient
|
||||
v-model:open="openPatient"
|
||||
v-model:selected="selectedPatient"
|
||||
:patients="patients"
|
||||
:pagination-meta="paginationMeta"
|
||||
@fetch="
|
||||
(value) => {
|
||||
if (value.search && value.search.length >= 3) {
|
||||
// Use identifier search for specific searches (NIK, RM, etc.)
|
||||
getPatientByIdentifierSearch(value.search)
|
||||
} else {
|
||||
// Use regular search for general searches
|
||||
getPatientsList({ ...value, 'page-size': 10, includes: 'person' })
|
||||
}
|
||||
}
|
||||
"
|
||||
@save="handleSavePatient"
|
||||
/>
|
||||
<AppSepTableSearchLetter
|
||||
v-model:open="openLetter"
|
||||
v-model:selected="selectedLetter"
|
||||
:letters="letters"
|
||||
@save="handleSaveLetter"
|
||||
/>
|
||||
<AppSepTableHistorySep
|
||||
<AppViewHistory
|
||||
v-model:open="openHistory"
|
||||
:histories="histories"
|
||||
/>
|
||||
|
||||
<AppViewLetter
|
||||
v-model:open="openLetter"
|
||||
:letters="letters"
|
||||
:menu="selectedAdmissionType !== '3' ? 'control' : 'reference'"
|
||||
:selected="selectedLetter"
|
||||
:pagination-meta="{ recordCount: 0, page: 1, pageSize: 10, totalPage: 0 } as any"
|
||||
@fetch="(value) => getLetterMappers(value.admissionType, value.search)"
|
||||
@save="handleSaveLetter"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+157
-175
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,183 +7,137 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '~/components/pub/ui/dropdown-menu'
|
||||
import AppSepList from '~/components/app/sep/list.vue'
|
||||
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
|
||||
|
||||
// Icons
|
||||
import { X, Check } from 'lucide-vue-next'
|
||||
|
||||
const search = ref('')
|
||||
const dateRange = ref('12 Agustus 2025 - 32 Agustus 2025')
|
||||
const open = ref(false)
|
||||
// Handlers
|
||||
import { useIntegrationSepList } from '~/handlers/integration-sep-list.handler'
|
||||
|
||||
const sepData = {
|
||||
no_sep: 'SP23311224',
|
||||
kartu: '001234',
|
||||
nama: 'Kenzie',
|
||||
}
|
||||
// Helpers
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
interface SepData {
|
||||
tgl_sep: string
|
||||
no_sep: string
|
||||
pelayanan: string
|
||||
jalur: string
|
||||
no_rm: string
|
||||
nama_pasien: string
|
||||
no_kartu_bpjs: string
|
||||
no_surat_kontrol: string
|
||||
tgl_surat_kontrol: string
|
||||
klinik_tujuan: string
|
||||
dpjp: string
|
||||
diagnosis_awal: string
|
||||
}
|
||||
// use handler to provide state and functions
|
||||
const {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
data,
|
||||
dateSelection,
|
||||
dateRange,
|
||||
serviceType,
|
||||
serviceTypesList,
|
||||
search,
|
||||
open,
|
||||
sepData,
|
||||
headerPrep,
|
||||
paginationMeta,
|
||||
isLoading,
|
||||
getSepList,
|
||||
setServiceTypes,
|
||||
setDateRange,
|
||||
handleExportCsv,
|
||||
handleExportExcel,
|
||||
handleRowSelected,
|
||||
handlePageChange,
|
||||
handleRemove,
|
||||
} = useIntegrationSepList()
|
||||
|
||||
const paginationMeta = reactive<PaginationMeta>({
|
||||
recordCount: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPage: 5,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
console.log('pageChange', page)
|
||||
}
|
||||
|
||||
const data = ref<SepData[]>([])
|
||||
|
||||
// contoh data dummy
|
||||
const rows = [
|
||||
{
|
||||
tgl_sep: '12 Agustus 2025',
|
||||
no_sep: 'SP23311224',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM23311224',
|
||||
nama_pasien: 'Ahmad Baidowi',
|
||||
no_kartu_bpjs: '334423231214',
|
||||
no_surat_kontrol: 'SK22334442',
|
||||
tgl_surat_kontrol: '13 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'dr. Andi Prasetyo, Sp.PD-KHOM',
|
||||
diagnosis_awal: 'C34.9 - Karsinoma Paru',
|
||||
},
|
||||
{
|
||||
tgl_sep: '12 Agustus 2025',
|
||||
no_sep: 'SP23311224',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM001234',
|
||||
nama_pasien: 'Kenzie',
|
||||
no_kartu_bpjs: '12301234',
|
||||
no_surat_kontrol: '123456',
|
||||
tgl_surat_kontrol: '10 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'Dr. Andreas Sutaji',
|
||||
diagnosis_awal: 'Bronchitis',
|
||||
},
|
||||
{
|
||||
tgl_sep: '11 Agustus 2025',
|
||||
no_sep: 'SP23455667',
|
||||
pelayanan: 'Rawat Jalan',
|
||||
jalur: 'Kontrol',
|
||||
no_rm: 'RM001009',
|
||||
nama_pasien: 'Abraham Sulaiman',
|
||||
no_kartu_bpjs: '334235',
|
||||
no_surat_kontrol: '123334',
|
||||
tgl_surat_kontrol: '11 Agustus 2024',
|
||||
klinik_tujuan: 'Penyakit dalam',
|
||||
dpjp: 'Dr. Andreas Sutaji',
|
||||
diagnosis_awal: 'Paru-paru basah',
|
||||
},
|
||||
]
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
// open filter modal
|
||||
},
|
||||
onInput: (_val: string) => {
|
||||
// filter patient list
|
||||
},
|
||||
onClear: () => {
|
||||
// clear url param
|
||||
},
|
||||
}
|
||||
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Daftar SEP Prosedur',
|
||||
icon: 'i-lucide-panel-bottom',
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => {
|
||||
navigateTo('/bpjs/sep/add')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async function getSepList() {
|
||||
isLoading.dataListLoading = true
|
||||
data.value = [...rows]
|
||||
isLoading.dataListLoading = false
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
console.log('Ekspor CSV dipilih')
|
||||
// tambahkan logic untuk generate CSV
|
||||
}
|
||||
|
||||
function exportExcel() {
|
||||
console.log('Ekspor Excel dipilih')
|
||||
// tambahkan logic untuk generate Excel
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
console.log('Data dihapus:', sepData)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => recAction.value,
|
||||
() => {
|
||||
if (recAction.value === 'showConfirmDel') {
|
||||
open.value = true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
getSepList()
|
||||
})
|
||||
const dataFiltered = ref<any>([])
|
||||
const debouncedSearch = refDebounced(search, 500)
|
||||
|
||||
// expose provides so component can also use provide/inject if needed
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
if (recAction.value === 'showConfirmDel') {
|
||||
open.value = true
|
||||
}
|
||||
if (recAction.value === 'showDetail') {
|
||||
navigateTo(`/integration/bpjs-vclaim/sep/${recItem.value?.letterNumber}/detail`)
|
||||
}
|
||||
})
|
||||
|
||||
watch(debouncedSearch, (newValue) => {
|
||||
if (newValue && newValue !== '-' && newValue.length >= 3) {
|
||||
dataFiltered.value = data.value.filter(
|
||||
(item: any) =>
|
||||
item.patientName.toLowerCase().includes(newValue.toLowerCase()) ||
|
||||
item.letterNumber.toLowerCase().includes(newValue.toLowerCase()) ||
|
||||
item.cardNumber.toLowerCase().includes(newValue.toLowerCase()),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => dateSelection.value,
|
||||
async (val) => {
|
||||
if (!val) return
|
||||
setDateRange()
|
||||
await getSepList()
|
||||
dataFiltered.value = [...data.value]
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => serviceType.value,
|
||||
() => {
|
||||
getSepList()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
setServiceTypes()
|
||||
await getSepList()
|
||||
dataFiltered.value = [...data.value]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-md border p-4">
|
||||
<div class="p-4">
|
||||
<Header :prep="{ ...headerPrep }" />
|
||||
<!-- Filter Bar -->
|
||||
<div class="my-2 flex flex-wrap items-center gap-2">
|
||||
<!-- Search -->
|
||||
<Input v-model="search" placeholder="Cari No. SEP / No. Kartu BPJS..." class="w-72" />
|
||||
<Input
|
||||
v-model="search"
|
||||
placeholder="Cari No. SEP / No. Kartu BPJS..."
|
||||
class="w-72"
|
||||
/>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="w-72">
|
||||
<Select
|
||||
id="serviceType"
|
||||
icon-name="i-lucide-chevron-down"
|
||||
v-model="serviceType"
|
||||
:items="serviceTypesList"
|
||||
placeholder="Pilih Pelayanan"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date Range -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-[40px] w-72 border-gray-400 bg-white text-right font-normal"
|
||||
>
|
||||
{{ dateRange }}
|
||||
<Icon name="i-lucide-calendar" class="h-5 w-5" />
|
||||
<Icon
|
||||
name="i-lucide-calendar"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-2">
|
||||
<Calendar mode="range" />
|
||||
<RangeCalendar v-model="dateSelection" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -196,52 +148,82 @@ provide('table_data_loader', isLoading)
|
||||
variant="outline"
|
||||
class="ml-auto h-[40px] w-[120px] rounded-md border-green-600 text-green-600 hover:bg-green-50"
|
||||
>
|
||||
<Icon name="i-lucide-download" class="h-5 w-5" /> Ekspor
|
||||
<Icon
|
||||
name="i-lucide-download"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
Ekspor
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-40">
|
||||
<DropdownMenuItem @click="exportCsv"> Ekspor CSV </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="exportExcel"> Ekspor Excel </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleExportCsv">Ekspor CSV</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleExportExcel">Ekspor Excel</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border p-4">
|
||||
<AppSepList v-if="!isLoading.dataListLoading" :data="data" />
|
||||
<div class="mt-4">
|
||||
<AppSepList
|
||||
v-if="!isLoading.dataListLoading"
|
||||
:data="dataFiltered"
|
||||
@update:modelValue="handleRowSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<template v-if="paginationMeta">
|
||||
<div v-if="paginationMeta.totalPage > 1">
|
||||
<PubMyUiPagination :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
<PubMyUiPagination
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Trigger button -->
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child></DialogTrigger>
|
||||
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Hapus SEP</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogDescription class="text-gray-700">
|
||||
Apakah anda yakin ingin menghapus SEP dengan data:
|
||||
</DialogDescription>
|
||||
|
||||
<DialogDescription class="text-gray-700">Apakah anda yakin ingin menghapus SEP dengan data:</DialogDescription>
|
||||
<div class="mt-4 space-y-2 text-sm">
|
||||
<p>No. SEP : {{ sepData.no_sep }}</p>
|
||||
<p>No. Kartu BPJS : {{ sepData.kartu }}</p>
|
||||
<p>Nama Pasien : {{ sepData.nama }}</p>
|
||||
<p>
|
||||
<strong>No. SEP:</strong>
|
||||
{{ sepData.sepNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>No. Kartu BPJS:</strong>
|
||||
{{ sepData.cardNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Nama Pasien:</strong>
|
||||
{{ sepData.patientName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="mt-6 flex justify-end gap-3">
|
||||
<Button variant="outline" class="border-green-600 text-green-600 hover:bg-green-50" @click="open = false">
|
||||
<X class="mr-1 h-4 w-4" /> Tidak
|
||||
<Button
|
||||
variant="outline"
|
||||
class="border-green-600 text-green-600 hover:bg-green-50"
|
||||
@click="
|
||||
() => {
|
||||
recId = 0
|
||||
recAction = ''
|
||||
open = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<X class="mr-1 h-4 w-4" />
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="destructive" class="bg-red-600 hover:bg-red-700" @click="handleDelete">
|
||||
<Check class="mr-1 h-4 w-4" /> Ya
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="bg-red-600 hover:bg-red-700"
|
||||
@click="handleRemove"
|
||||
>
|
||||
<Check class="mr-1 h-4 w-4" />
|
||||
Ya
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useQueryMode } from '@/composables/useQueryMode' // asumsikan ini ada
|
||||
import { useQueryMode } from '@/composables/useQueryMode'
|
||||
|
||||
import SoapiList from './list.vue'
|
||||
import EarlyForm from './form.vue'
|
||||
@@ -11,7 +11,7 @@ import FunctionForm from './form-function.vue'
|
||||
const route = useRoute()
|
||||
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
||||
|
||||
const { mode, openForm, backToList } = useQueryMode('mode')
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
const formMap = {
|
||||
'early-medical-assessment': EarlyForm,
|
||||
@@ -24,11 +24,7 @@ const ActiveForm = computed(() => formMap[type.value] || EarlyForm)
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SoapiList
|
||||
v-if="mode === 'list'"
|
||||
@add="openForm"
|
||||
/>
|
||||
|
||||
<SoapiList v-if="mode === 'list'" />
|
||||
<component
|
||||
v-else
|
||||
:is="ActiveForm"
|
||||
|
||||
@@ -1,45 +1,209 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { FunctionSoapiSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const isOpenFungsional = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const fungsional = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const selectedFungsional = ref<any>(null)
|
||||
const schema = FunctionSoapiSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
typeCode: 'function',
|
||||
value: '',
|
||||
})
|
||||
const model = ref({
|
||||
'prim-compl': '',
|
||||
'past-disease': '',
|
||||
'current-disease': '',
|
||||
gcs: '',
|
||||
'respiratory-rate': '',
|
||||
'respiratory-rate-type': '',
|
||||
pulse: '',
|
||||
'pulse-type': '',
|
||||
'right-arm-bp': '',
|
||||
'left-arm-bp': '',
|
||||
'axillary-temp': '',
|
||||
'rektal-temp': '',
|
||||
skin: '',
|
||||
head: '',
|
||||
ear: '',
|
||||
nose: '',
|
||||
'oral-cavity': '',
|
||||
eye: '',
|
||||
'other-body-part': '',
|
||||
neck: '',
|
||||
thyroid: '',
|
||||
thorax: '',
|
||||
heart: '',
|
||||
lung: '',
|
||||
abdomen: '',
|
||||
heart2: '',
|
||||
lien: '',
|
||||
back: '',
|
||||
extremity: '',
|
||||
gender: '',
|
||||
rectum: '',
|
||||
'system-syaraf': '',
|
||||
'nervous-system': '',
|
||||
'cardio-respiratory': '',
|
||||
imaging: '',
|
||||
laboratory: '',
|
||||
})
|
||||
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleClick(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
} else if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
const entryRehabRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRehabRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (
|
||||
selectedProcedure.value?.length > 0 ||
|
||||
selectedDiagnose.value?.length > 0 ||
|
||||
selectedFungsional.value?.length > 0
|
||||
) {
|
||||
result.data.procedure = selectedProcedure.value || []
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
result.data.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{},
|
||||
toast,
|
||||
)
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
icdPreview.value.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="entryRehabRef"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="function"
|
||||
:exclude-fields="['prim-compl', 'sec-compl']"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
v-model:open="isOpenProcedure"
|
||||
title="Pilih Prosedur"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedProcedure"
|
||||
:data="procedures"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Diagnosa"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenFungsional"
|
||||
title="Pilih Fungsional"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedFungsional"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,45 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { EarlyRehabSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
const { backToList } = useQueryMode('mode')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const schema = EarlyRehabSchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
typeCode: 'early-rehab',
|
||||
value: '',
|
||||
})
|
||||
const model = ref({
|
||||
'prim-compl': '',
|
||||
'medical-plan': '',
|
||||
'diagnosis-medical': '',
|
||||
'rehab-trouble': '',
|
||||
'medical-trouble': '',
|
||||
'physic-modal': '',
|
||||
exercise: '',
|
||||
'ortho-pesa': '',
|
||||
education: '',
|
||||
other: '',
|
||||
cranialis: '',
|
||||
sensoris: '',
|
||||
'reflect-fisio': '',
|
||||
'reflect-pato': '',
|
||||
otonom: '',
|
||||
localis: '',
|
||||
'medical-trial': '',
|
||||
therapy: '',
|
||||
'syst-bp': '',
|
||||
'diast-bp': '',
|
||||
pulse: '',
|
||||
gcs: '',
|
||||
'respiratory-rate': '',
|
||||
temperature: '',
|
||||
ambulance: '',
|
||||
gait: '',
|
||||
'neck-rom': '',
|
||||
'body-rom': '',
|
||||
'aga-rom': '',
|
||||
'agb-rom': '',
|
||||
'neck-mmt': '',
|
||||
'body-mmt': '',
|
||||
'aga-mmt': '',
|
||||
'agb-mmt': '',
|
||||
localis: '',
|
||||
'medical-trouble': '',
|
||||
'rehab-trouble': '',
|
||||
temp: '',
|
||||
spo2: '',
|
||||
weight: '',
|
||||
height: '',
|
||||
})
|
||||
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleClick(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
function handleOpen(type: string) {
|
||||
if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const entryRehabRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRehabRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (selectedDiagnose.value?.length > 0) {
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
}
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{},
|
||||
toast,
|
||||
)
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="entryRehabRef"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="early-rehab"
|
||||
:exclude-fields="['prim-compl', 'sec-compl']"
|
||||
@click="handleClick"
|
||||
@click="handleOpen"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpen"
|
||||
title="Pilih Prosedur"
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Fungsional"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,36 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/soapi/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { EarlySchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
|
||||
const isOpen = ref(false)
|
||||
const data = ref([])
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
const { recordId } = useQueryCRUDMode('record-id')
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const schema = EarlySchema
|
||||
const payload = ref({
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
typeCode: 'early-medic',
|
||||
value: '',
|
||||
})
|
||||
|
||||
const model = ref({
|
||||
'prim-compl': '',
|
||||
'cur-disea-hist': '',
|
||||
'syst-bp': '',
|
||||
'diast-bp': '',
|
||||
pulse: '',
|
||||
'resp-rate': '',
|
||||
temp: '',
|
||||
weight: '',
|
||||
height: '',
|
||||
'reflect-fisio': '',
|
||||
'reflect-pato': '',
|
||||
'autonom-neuron': '',
|
||||
'medical-act': '',
|
||||
therapy: '',
|
||||
})
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getPatientList() {
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const resp = await xfetch(`/api/v1/soapi/${recordId}`)
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
getProcedures()
|
||||
getDiagnoses()
|
||||
})
|
||||
|
||||
function handleClick(type: string) {
|
||||
function handleOpen(type: string) {
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const entryRef = ref()
|
||||
async function actionHandler(type: string) {
|
||||
console.log(type)
|
||||
isOpen.value = true
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
const result = await entryRef.value?.validate()
|
||||
if (result?.valid) {
|
||||
if (selectedProcedure.value?.length > 0 || selectedDiagnose.value?.length > 0) {
|
||||
result.data.procedure = selectedProcedure.value || []
|
||||
result.data.diagnose = selectedDiagnose.value || []
|
||||
}
|
||||
|
||||
console.log('data', result.data)
|
||||
handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{},
|
||||
toast,
|
||||
)
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
icdPreview.value.procedures = selectedProcedure.value || []
|
||||
icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry type="early" :exclude-fields="['prim-compl', 'sec-compl']" @click="handleClick" />
|
||||
<Dialog v-model:open="isOpen" title="Pilih Prosedur" size="xl" prevent-outside>
|
||||
<AppIcdMultiselectPicker :data="data" />
|
||||
<Entry
|
||||
ref="entryRef"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
type="early"
|
||||
@modal="handleOpen"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpenProcedure"
|
||||
title="Pilih Prosedur"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedProcedure"
|
||||
:data="procedures"
|
||||
/>
|
||||
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Pilih Diagnosa"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<AppIcdMultiselectPicker
|
||||
v-model:model-value="selectedDiagnose"
|
||||
:data="diagnoses"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<ActionDialog @click="actionDialogHandler" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import AssesmentFunctionList from '~/components/app/soapi/list.vue'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
import { ActionEvents, type HeaderPrep, type RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/soapi/list.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import { handleActionRemove } from '~/handlers/soapi-early.handler'
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
label: string
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
|
||||
const { recordId } = useQueryCRUDRecordId()
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const data = ref([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const isLoading = ref(false)
|
||||
const isReadonly = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const paginationMeta = ref<PaginationMeta>(null)
|
||||
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
@@ -24,34 +53,115 @@ const refSearchNav: RefSearchNav = {
|
||||
},
|
||||
}
|
||||
|
||||
// Loading state management
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
const typeCode = ref('')
|
||||
|
||||
const hreaderPrep: HeaderPrep = {
|
||||
title: props.label,
|
||||
icon: 'i-lucide-users',
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
onClick: () => emits('add'),
|
||||
onClick: () => goToEntry(),
|
||||
},
|
||||
}
|
||||
|
||||
async function getPatientList() {
|
||||
const resp = await xfetch('/api/v1/patient')
|
||||
const type = computed(() => (route.query.tab as string) || 'early-medical-assessment')
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
|
||||
async function getMyList() {
|
||||
const url = `/api/v1/soapi?type-code=${typeCode.value}&includes=encounter,employee&encounter-id=${route.params.id}`
|
||||
const resp = await xfetch(url)
|
||||
if (resp.success) {
|
||||
data.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPatientList()
|
||||
const goEdit = (id: string) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emits('pageChange', page)
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
recordId.value = ''
|
||||
backToList()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => type.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (val === 'early-medical-assessment') {
|
||||
typeCode.value = 'early-medic'
|
||||
} else if (val === 'rehab-medical-assessment') {
|
||||
typeCode.value = 'early-rehab'
|
||||
} else if (val === 'function-assessment') {
|
||||
typeCode.value = 'function'
|
||||
}
|
||||
getMyList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
emits('edit')
|
||||
isReadonly.value = false
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': recId.value,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case ActionEvents.showDetail:
|
||||
emits('edit')
|
||||
isReadonly.value = true
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': recId.value,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
|
||||
case ActionEvents.showAdd:
|
||||
recordId.value = ''
|
||||
goToEntry()
|
||||
emits('add')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// watch(recId, () => {
|
||||
// console.log('recId', recId.value)
|
||||
// console.log('recIdaacin', recAction.value)
|
||||
// if (recAction.value === 'showEdit') {
|
||||
// goEdit(recId.value)
|
||||
// }
|
||||
// })
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
@@ -59,7 +169,39 @@ provide('table_data_loader', isLoading)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
|
||||
<Header
|
||||
:prep="{ ...hreaderPrep }"
|
||||
:ref-search-nav="refSearchNav"
|
||||
/>
|
||||
<List :data="data" />
|
||||
|
||||
<AssesmentFunctionList :data="data" />
|
||||
<PaginationView
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import AppSpecialistPositionList from '~/components/app/specialist-position/list.vue'
|
||||
import AppSpecialistPositionEntryForm from '~/components/app/specialist-position/entry-form.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { config } from '~/components/app/specialist-position/list.cfg'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { type SpecialistPositionFormData, SpecialistPositionSchema } from '~/schemas/specialist-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/specialist-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/specialist-position.service'
|
||||
import { getValueLabelList as getValueLabelSpecialistList } from '~/services/specialist.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const specialists = ref<{ value: string | number; label: string }[]>([])
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getSpecialistList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'specialist,Employee.Person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'specialist-position',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Spesialis - Posisi',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getCurrentSpecialistDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Detail Spesialis Posisi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Edit Spesialis Posisi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
specialists.value = await getValueLabelSpecialistList({ sort: 'createdAt:asc', 'page-size': 100 }, true)
|
||||
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<AppSpecialistPositionList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Spesialis'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppSpecialistPositionEntryForm
|
||||
:schema="SpecialistPositionSchema"
|
||||
:specialists="specialists"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getSpecialistList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getSpecialistList, resetForm, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getSpecialistList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
// Service
|
||||
import type { Specialist } from '~/models/specialist'
|
||||
import { getDetail as getDetailSpecialist } from '~/services/specialist.service'
|
||||
|
||||
// #region specialist positions
|
||||
import { config } from '~/components/app/specialist/detail/list.cfg'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
|
||||
// Types
|
||||
import { type SpecialistPositionFormData, SpecialistPositionSchema } from '~/schemas/specialist-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/specialist-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail as getDetailSpecialistPosition } from '~/services/specialist-position.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
|
||||
const title = ref('')
|
||||
// #endregion
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
specialistId: string
|
||||
}>()
|
||||
const specialist = ref<Specialist>({} as Specialist)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getPositionList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
console.log(props.specialistId)
|
||||
const result = await getList({
|
||||
'specialist-id': props.specialistId,
|
||||
includes: 'Employee.Person',
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-no-limit': true,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'specialist-position',
|
||||
})
|
||||
|
||||
const dataMap = computed(() => {
|
||||
return data.value.map((v, i) => {
|
||||
return {
|
||||
...v,
|
||||
index: i + 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Spesialis',
|
||||
icon: 'i-lucide-user',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah Posisi',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await getDetailSpecialist(props.specialistId, {
|
||||
includes: 'unit',
|
||||
})
|
||||
if (result.success) {
|
||||
specialist.value = result.body.data || {}
|
||||
}
|
||||
|
||||
const res = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
employees.value = res
|
||||
} catch (err) {
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
getDetailSpecialistPosition(recItem.value.code)
|
||||
title.value = 'Edit Posisi'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
|
||||
<AppSpecialistDetail :specialist="specialist" />
|
||||
<div class="h-6"></div>
|
||||
|
||||
<AppSpecialistDetailList
|
||||
:data="dataMap"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Posisi'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppSpecialistPositionEntryDetail
|
||||
:schema="SpecialistPositionSchema"
|
||||
:specialist-id="specialistId"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getPositionList, onResetState, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getPositionList, onResetState, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getPositionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -1,95 +0,0 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const schemaConf = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: 'Nama spesialisasi harus diisi',
|
||||
})
|
||||
.min(3, 'Nama spesialisasi minimal 3 karakter'),
|
||||
|
||||
code: z
|
||||
.string({
|
||||
required_error: 'Kode spesialisasi harus diisi',
|
||||
})
|
||||
.min(3, 'Kode spesialisasi minimal 3 karakter'),
|
||||
|
||||
installationId: z
|
||||
.string({
|
||||
required_error: 'Instalasi harus dipilih',
|
||||
})
|
||||
.min(1, 'Instalasi harus dipilih'),
|
||||
|
||||
unitId: z
|
||||
.string({
|
||||
required_error: 'Unit harus dipilih',
|
||||
})
|
||||
.min(1, 'Unit harus dipilih'),
|
||||
})
|
||||
|
||||
// Unit mapping berdasarkan installation
|
||||
export const installationUnitMapping: Record<string, string[]> = {
|
||||
'1': ['1', '3', '5'],
|
||||
'2': ['2', '4', '6'],
|
||||
'3': ['7', '8', '9', '10', '11'],
|
||||
}
|
||||
|
||||
export const unitConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih unit',
|
||||
search: 'kode, nama unit',
|
||||
empty: 'unit tidak ditemukan',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
|
||||
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
|
||||
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
|
||||
{ value: '4', label: 'Instalasi Penunjang Non-Medis', code: 'SUP' },
|
||||
{ value: '5', label: 'Instalasi Pendidikan & Pelatihan', code: 'EDU' },
|
||||
{ value: '6', label: 'Instalasi Farmasi', code: 'PHA' },
|
||||
{ value: '7', label: 'Instalasi Radiologi', code: 'RAD' },
|
||||
{ value: '8', label: 'Instalasi Laboratorium', code: 'LAB' },
|
||||
{ value: '9', label: 'Instalasi Keuangan', code: 'FIN' },
|
||||
{ value: '10', label: 'Instalasi SDM', code: 'HR' },
|
||||
{ value: '11', label: 'Instalasi Teknologi Informasi', code: 'ITS' },
|
||||
{ value: '12', label: 'Instalasi Pemeliharaan & Sarana', code: 'MNT' },
|
||||
{ value: '13', label: 'Instalasi Gizi / Catering', code: 'CAT' },
|
||||
{ value: '14', label: 'Instalasi Keamanan', code: 'SEC' },
|
||||
{ value: '15', label: 'Instalasi Gawat Darurat', code: 'EMR' },
|
||||
{ value: '16', label: 'Instalasi Bedah Sentral', code: 'SUR' },
|
||||
{ value: '17', label: 'Instalasi Rawat Jalan', code: 'OUT' },
|
||||
{ value: '18', label: 'Instalasi Rawat Inap', code: 'INP' },
|
||||
{ value: '19', label: 'Instalasi Rehabilitasi Medik', code: 'REB' },
|
||||
{ value: '20', label: 'Instalasi Penelitian & Pengembangan', code: 'RSH' },
|
||||
],
|
||||
}
|
||||
|
||||
export const installationConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih instalasi',
|
||||
search: 'kode, nama instalasi',
|
||||
empty: 'instalasi tidak ditemukan',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Ambulatory', code: 'AMB' },
|
||||
{ value: '2', label: 'Inpatient', code: 'IMP' },
|
||||
{ value: '3', label: 'Emergency', code: 'EMER' },
|
||||
],
|
||||
}
|
||||
|
||||
// Helper function untuk filter unit berdasarkan installation
|
||||
export function getFilteredUnits(installationId: string) {
|
||||
if (!installationId || !installationUnitMapping[installationId]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allowedUnitIds = installationUnitMapping[installationId]
|
||||
return unitConf.items.filter((unit) => allowedUnitIds.includes(unit.value))
|
||||
}
|
||||
|
||||
// Helper function untuk membuat unit config yang ter-filter
|
||||
export function createFilteredUnitConf(installationId: string) {
|
||||
return {
|
||||
...unitConf,
|
||||
items: getFilteredUnits(installationId),
|
||||
}
|
||||
}
|
||||
@@ -103,12 +103,23 @@ const getCurrentSpecialistDetail = async (id: number | string) => {
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentSpecialistDetail(recId.value)
|
||||
title.value = 'Detail Spesialis'
|
||||
isReadonly.value = true
|
||||
if (recItem.value.code.length > 0) {
|
||||
navigateTo({
|
||||
name: 'org-src-specialist-id',
|
||||
params: {
|
||||
id: recItem.value.code,
|
||||
},
|
||||
})
|
||||
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = false
|
||||
isReadonly.value = false
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentSpecialistDetail(recId.value)
|
||||
getCurrentSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Edit Spesialis'
|
||||
isReadonly.value = false
|
||||
break
|
||||
@@ -119,7 +130,7 @@ watch([recId, recAction], () => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
units.value = await getUnitList()
|
||||
units.value = await getUnitList({}, true)
|
||||
await getSpecialistList()
|
||||
})
|
||||
</script>
|
||||
@@ -158,8 +169,8 @@ onMounted(async () => {
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SpecialistFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getSpecialistList, resetForm, toast)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getSpecialistList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getSpecialistList, resetForm, toast)
|
||||
@@ -174,7 +185,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getSpecialistList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getSpecialistList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import AppSubSpecialistPositionList from '~/components/app/subspecialist-position/list.vue'
|
||||
import AppSubSpecialistPositionEntryForm from '~/components/app/subspecialist-position/entry-form.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { config } from '~/components/app/subspecialist-position/list.cfg'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import {
|
||||
type SubSpecialistPositionFormData,
|
||||
SubSpecialistPositionSchema,
|
||||
} from '~/schemas/subspecialist-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/subspecialist-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/subspecialist-position.service'
|
||||
import { getValueLabelList as getValueLabelSubSpecialistList } from '~/services/subspecialist.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const subSpecialists = ref<{ value: string | number; label: string }[]>([])
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getSubSpecialistList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
const result = await getList({
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-number': params['page-number'] || 0,
|
||||
'page-size': params['page-size'] || 10,
|
||||
includes: 'subspecialist,Employee.Person',
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'subspecialist-position',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Sub Spesialis - Posisi',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getCurrentSubSpecialistDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
console.log(result)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentSubSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Detail Sub Spesialis Posisi'
|
||||
isReadonly.value = true
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentSubSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Edit Sub Spesialis Posisi'
|
||||
isReadonly.value = false
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
subSpecialists.value = await getValueLabelSubSpecialistList({ sort: 'createdAt:asc', 'page-size': 100 }, true)
|
||||
employees.value = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<AppSubSpecialistPositionList
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Sub Spesialis'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppSubSpecialistPositionEntryForm
|
||||
:schema="SubSpecialistPositionSchema"
|
||||
:sub-specialists="subSpecialists"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SubSpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getSubSpecialistList, onResetState, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getSubSpecialistList, onResetState, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getSubSpecialistList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
|
||||
// Service
|
||||
import type { Subspecialist } from '~/models/subspecialist'
|
||||
import { getDetail as getDetailSubSpecialist } from '~/services/subspecialist.service'
|
||||
|
||||
// #region subspecialist positions
|
||||
import { config } from '~/components/app/subspecialist/detail/list.cfg'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
|
||||
// Types
|
||||
import {
|
||||
type SubSpecialistPositionFormData,
|
||||
SubSpecialistPositionSchema,
|
||||
} from '~/schemas/subspecialist-position.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/subspecialist-position.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail as getDetailSubSpecialistPosition } from '~/services/subspecialist-position.service'
|
||||
import { getValueLabelList as getEmployeeLabelList } from '~/services/employee.service'
|
||||
|
||||
const employees = ref<{ value: string | number; label: string }[]>([])
|
||||
|
||||
const title = ref('')
|
||||
// #endregion
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
subspecialistId: string
|
||||
}>()
|
||||
const subSpecialist = ref<Subspecialist>({} as Subspecialist)
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getPositionList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async (params: any) => {
|
||||
console.log(props.subspecialistId)
|
||||
const result = await getList({
|
||||
'subspecialist-id': props.subspecialistId,
|
||||
includes: 'Employee.Person',
|
||||
search: params.search,
|
||||
sort: 'createdAt:asc',
|
||||
'page-no-limit': true,
|
||||
})
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'subspecialist-position',
|
||||
})
|
||||
|
||||
const dataMap = computed(() => {
|
||||
return data.value.map((v, i) => {
|
||||
return {
|
||||
...v,
|
||||
index: i + 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Sub Spesialis',
|
||||
icon: 'i-lucide-user',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah Posisi',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = true
|
||||
isReadonly.value = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await getDetailSubSpecialist(props.subspecialistId, {
|
||||
includes: 'specialist,specialist.Unit,specialist.Unit.Installation',
|
||||
})
|
||||
if (result.success) {
|
||||
subSpecialist.value = result.body.data || {}
|
||||
}
|
||||
|
||||
const res = await getEmployeeLabelList({ sort: 'createdAt:asc', 'page-size': 100, includes: 'person' })
|
||||
employees.value = res
|
||||
} catch (err) {
|
||||
// show toast
|
||||
toast({
|
||||
title: 'Terjadi Kesalahan',
|
||||
description: 'Terjadi kesalahan saat memuat data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showEdit:
|
||||
getDetailSubSpecialistPosition(recItem.value.code)
|
||||
title.value = 'Edit Posisi'
|
||||
isReadonly.value = false
|
||||
isFormEntryDialogOpen.value = true
|
||||
break
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
/>
|
||||
|
||||
<AppSubspecialistDetail :subspecialist="subSpecialist" />
|
||||
<div class="h-6"></div>
|
||||
|
||||
<AppSubspecialistDetailList
|
||||
:data="dataMap"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
v-model:open="isFormEntryDialogOpen"
|
||||
:title="!!recItem ? title : 'Tambah Posisi'"
|
||||
size="lg"
|
||||
prevent-outside
|
||||
@update:open="
|
||||
(value: any) => {
|
||||
onResetState()
|
||||
isFormEntryDialogOpen = value
|
||||
}
|
||||
"
|
||||
>
|
||||
<AppSubspecialistPositionEntryDetail
|
||||
:schema="SubSpecialistPositionSchema"
|
||||
:subspecialist-id="subspecialistId"
|
||||
:employees="employees"
|
||||
:values="recItem"
|
||||
:is-loading="isProcessing"
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SubSpecialistPositionFormData | Record<string, any>, resetForm: () => void) => {
|
||||
console.log(values)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getPositionList, onResetState, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getPositionList, onResetState, toast)
|
||||
}
|
||||
"
|
||||
@cancel="handleCancelForm"
|
||||
/>
|
||||
</Dialog>
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recItem.code, getPositionList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p
|
||||
v-for="field in config.delKeyNames"
|
||||
:key="field.key"
|
||||
:v-if="record?.[field.key]"
|
||||
>
|
||||
<span class="font-semibold">{{ field.label }}:</span>
|
||||
{{ record[field.key] }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -1,98 +0,0 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const schemaConf = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: 'Nama spesialisasi harus diisi',
|
||||
})
|
||||
.min(3, 'Nama spesialisasi minimal 3 karakter'),
|
||||
|
||||
code: z
|
||||
.string({
|
||||
required_error: 'Kode spesialisasi harus diisi',
|
||||
})
|
||||
.min(3, 'Kode spesialisasi minimal 3 karakter'),
|
||||
|
||||
installationId: z
|
||||
.string({
|
||||
required_error: 'Instalasi harus dipilih',
|
||||
})
|
||||
.min(1, 'Instalasi harus dipilih'),
|
||||
|
||||
unitId: z
|
||||
.string({
|
||||
required_error: 'Unit harus dipilih',
|
||||
})
|
||||
.min(1, 'Unit harus dipilih'),
|
||||
specialistId: z
|
||||
.string({
|
||||
required_error: 'Specialist harus dipilih',
|
||||
})
|
||||
.min(1, 'Specialist harus dipilih'),
|
||||
})
|
||||
|
||||
// Unit mapping berdasarkan installation
|
||||
export const installationUnitMapping: Record<string, string[]> = {
|
||||
'1': ['1', '3'],
|
||||
'2': ['2', '3'],
|
||||
'3': ['1', '2', '3'],
|
||||
}
|
||||
|
||||
export const unitConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih unit',
|
||||
search: 'kode, nama unit',
|
||||
empty: 'unit tidak ditemukan',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Instalasi Medis', code: 'MED' },
|
||||
{ value: '2', label: 'Instalasi Keperawatan', code: 'NUR' },
|
||||
{ value: '3', label: 'Instalasi Administrasi', code: 'ADM' },
|
||||
],
|
||||
}
|
||||
|
||||
export const specialistConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih specialist',
|
||||
search: 'kode, nama specialist',
|
||||
empty: 'specialist tidak ditemukan',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Spesialis Jantung', code: 'CARD' },
|
||||
{ value: '2', label: 'Spesialis Mata', code: 'OPHT' },
|
||||
{ value: '3', label: 'Spesialis Bedah', code: 'SURG' },
|
||||
{ value: '4', label: 'Spesialis Anak', code: 'PEDI' },
|
||||
{ value: '5', label: 'Spesialis Kandungan', code: 'OBGY' },
|
||||
],
|
||||
}
|
||||
|
||||
export const installationConf = {
|
||||
msg: {
|
||||
placeholder: '---pilih instalasi',
|
||||
search: 'kode, nama instalasi',
|
||||
empty: 'instalasi tidak ditemukan',
|
||||
},
|
||||
items: [
|
||||
{ value: '1', label: 'Ambulatory', code: 'AMB' },
|
||||
{ value: '2', label: 'Inpatient', code: 'IMP' },
|
||||
{ value: '3', label: 'Emergency', code: 'EMER' },
|
||||
],
|
||||
}
|
||||
|
||||
// Helper function untuk filter unit berdasarkan installation
|
||||
export function getFilteredUnits(installationId: string) {
|
||||
if (!installationId || !installationUnitMapping[installationId]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allowedUnitIds = installationUnitMapping[installationId]
|
||||
return unitConf.items.filter((unit) => allowedUnitIds.includes(unit.value))
|
||||
}
|
||||
|
||||
// Helper function untuk membuat unit config yang ter-filter
|
||||
export function createFilteredUnitConf(installationId: string) {
|
||||
return {
|
||||
...unitConf,
|
||||
items: getFilteredUnits(installationId),
|
||||
}
|
||||
}
|
||||
@@ -103,12 +103,23 @@ const getCurrentSubSpecialistDetail = async (id: number | string) => {
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
getCurrentSubSpecialistDetail(recId.value)
|
||||
title.value = 'Detail Sub Spesialis'
|
||||
isReadonly.value = true
|
||||
if (recItem.value.code.length > 0) {
|
||||
navigateTo({
|
||||
name: 'org-src-subspecialist-id',
|
||||
params: {
|
||||
id: recItem.value.code,
|
||||
},
|
||||
})
|
||||
|
||||
recAction.value = ''
|
||||
recItem.value = null
|
||||
recId.value = 0
|
||||
isFormEntryDialogOpen.value = false
|
||||
isReadonly.value = false
|
||||
}
|
||||
break
|
||||
case ActionEvents.showEdit:
|
||||
getCurrentSubSpecialistDetail(recId.value)
|
||||
getCurrentSubSpecialistDetail(recItem.value.code)
|
||||
title.value = 'Edit Sub Spesialis'
|
||||
isReadonly.value = false
|
||||
break
|
||||
@@ -119,7 +130,7 @@ watch([recId, recAction], () => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
specialists.value = await getSpecialistsList()
|
||||
specialists.value = await getSpecialistsList({}, true)
|
||||
await getSubSpecialistList()
|
||||
})
|
||||
</script>
|
||||
@@ -158,8 +169,8 @@ onMounted(async () => {
|
||||
:is-readonly="isReadonly"
|
||||
@submit="
|
||||
(values: SubspecialistFormData | Record<string, any>, resetForm: () => void) => {
|
||||
if (recId > 0) {
|
||||
handleActionEdit(recId, values, getSubSpecialistList, resetForm, toast)
|
||||
if (recItem?.code.length > 0) {
|
||||
handleActionEdit(recItem.code, values, getSubSpecialistList, resetForm, toast)
|
||||
return
|
||||
}
|
||||
handleActionSave(values, getSubSpecialistList, resetForm, toast)
|
||||
@@ -174,7 +185,7 @@ onMounted(async () => {
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getSubSpecialistList, toast)"
|
||||
@confirm="() => handleActionRemove(recItem.code, getSubSpecialistList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useQueryMode } from '@/composables/useQueryMode'
|
||||
|
||||
import List from './list.vue'
|
||||
import Form from './form.vue'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const route = useRoute()
|
||||
|
||||
const { mode, goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<List
|
||||
v-if="mode === 'list'"
|
||||
:encounter="props.encounter"
|
||||
@add="goToEntry"
|
||||
@edit="goToEntry"
|
||||
/>
|
||||
<Form
|
||||
v-else
|
||||
@back="backToList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import Entry from '~/components/app/summary-medic/entry.vue'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { SummaryMedicSchema } from '~/schemas/soapi.schema'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { handleActionSave, handleActionEdit } from '~/handlers/soapi-early.handler'
|
||||
// Services
|
||||
import { getDetail } from '~/services/summary-medic.service'
|
||||
const { backToList } = useQueryCRUDMode('mode')
|
||||
const { recordId } = useQueryCRUDRecordId('record-id')
|
||||
|
||||
const route = useRoute()
|
||||
const isOpenProcedure = ref(false)
|
||||
const isOpenDiagnose = ref(false)
|
||||
const isOpenFungsional = ref(false)
|
||||
const procedures = ref([])
|
||||
const diagnoses = ref([])
|
||||
const fungsional = ref([])
|
||||
const selectedProcedure = ref<any>(null)
|
||||
const selectedDiagnose = ref<any>(null)
|
||||
const selectedFungsional = ref<any>(null)
|
||||
const schema = SummaryMedicSchema
|
||||
const payload = ref({
|
||||
typeCode: 'amb-resume',
|
||||
encounter_id: 0,
|
||||
time: '',
|
||||
value: '',
|
||||
})
|
||||
const model = ref({
|
||||
date: '',
|
||||
doctor: '',
|
||||
diagnosis: '',
|
||||
essay: '',
|
||||
plan: '',
|
||||
note: '',
|
||||
})
|
||||
|
||||
const fileUrl = ref('')
|
||||
const isLoading = reactive<DataTableLoader>({
|
||||
isTableLoading: false,
|
||||
})
|
||||
|
||||
async function getDiagnoses() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/diagnose-src')
|
||||
if (resp.success) {
|
||||
diagnoses.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
async function getProcedures() {
|
||||
isLoading.isTableLoading = true
|
||||
const resp = await xfetch('/api/v1/procedure-src')
|
||||
if (resp.success) {
|
||||
procedures.value = (resp.body as Record<string, any>).data
|
||||
}
|
||||
isLoading.isTableLoading = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const mode = route.query.mode
|
||||
const recordId = route.query['record-id']
|
||||
if (mode === 'entry' && recordId) {
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: mapping data detail when edit
|
||||
const loadEntryForEdit = async (id: number) => {
|
||||
const result = await getDetail(id)
|
||||
|
||||
if (result?.success) {
|
||||
console.log('model', model.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(type: string) {
|
||||
if (type === 'prosedur') {
|
||||
isOpenProcedure.value = true
|
||||
} else if (type === 'diagnosa') {
|
||||
isOpenDiagnose.value = true
|
||||
} else if (type === 'fungsional') {
|
||||
isOpenDiagnose.value = true
|
||||
}
|
||||
}
|
||||
const entryGeneralConsent = ref()
|
||||
async function actionHandler(type: string) {
|
||||
if (type === 'back') {
|
||||
backToList()
|
||||
return
|
||||
}
|
||||
if (type === 'print') {
|
||||
return
|
||||
}
|
||||
const result = await entryGeneralConsent.value?.validate()
|
||||
if (result?.valid) {
|
||||
// if (result.data.relatives.length > 0) {
|
||||
// result.data.relatives = result.data.relatives.map((item: any) => {
|
||||
// return item.name
|
||||
// })
|
||||
// }
|
||||
|
||||
console.log('data', result)
|
||||
const resp = await handleActionSave(
|
||||
{
|
||||
...payload.value,
|
||||
value: JSON.stringify(result.data),
|
||||
encounter_id: +route.params.id,
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
toast,
|
||||
)
|
||||
backToList()
|
||||
} else {
|
||||
console.log('Ada error di form', result)
|
||||
}
|
||||
}
|
||||
|
||||
const icdPreview = ref({
|
||||
procedures: [],
|
||||
diagnoses: [],
|
||||
})
|
||||
|
||||
function actionDialogHandler(type: string) {
|
||||
if (type === 'submit') {
|
||||
// icdPreview.value.procedures = selectedProcedure.value || []
|
||||
// icdPreview.value.diagnoses = selectedDiagnose.value || []
|
||||
// icdPreview.value.fungsional = selectedFungsional.value || []
|
||||
}
|
||||
isOpenProcedure.value = false
|
||||
isOpenDiagnose.value = false
|
||||
}
|
||||
|
||||
provide('table_data_loader', isLoading)
|
||||
provide('icdPreview', icdPreview)
|
||||
</script>
|
||||
<template>
|
||||
<Entry
|
||||
ref="entryGeneralConsent"
|
||||
v-model="model"
|
||||
:schema="schema"
|
||||
@click="handleClick"
|
||||
/>
|
||||
<div class="my-2 flex justify-end py-2">
|
||||
<Action @click="actionHandler" />
|
||||
</div>
|
||||
<Dialog
|
||||
v-model:open="isOpenDiagnose"
|
||||
title="Preview General Content"
|
||||
size="xl"
|
||||
prevent-outside
|
||||
>
|
||||
<embed
|
||||
style="width: 100%; height: 90vh"
|
||||
:src="fileUrl"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
// Components
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import List from '~/components/app/soapi/list.vue'
|
||||
import Entry from '~/components/app/summary-medic/entry.vue'
|
||||
|
||||
// Helpers
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
|
||||
// Types
|
||||
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
|
||||
import { GeneralConsentSchema, type GeneralConsentFormData } from '~/schemas/general-consent.schema'
|
||||
|
||||
// Handlers
|
||||
import {
|
||||
recId,
|
||||
recAction,
|
||||
recItem,
|
||||
isReadonly,
|
||||
isProcessing,
|
||||
isFormEntryDialogOpen,
|
||||
isRecordConfirmationOpen,
|
||||
onResetState,
|
||||
handleActionSave,
|
||||
handleActionEdit,
|
||||
handleActionRemove,
|
||||
handleCancelForm,
|
||||
} from '~/handlers/summary-medic.handler'
|
||||
|
||||
// Services
|
||||
import { getList, getDetail } from '~/services/soapi-early.service'
|
||||
|
||||
// Models
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
encounter: Encounter
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits(['add', 'edit'])
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const { goToEntry, backToList } = useQueryCRUDMode('mode')
|
||||
|
||||
let units = ref<{ value: string; label: string }[]>([])
|
||||
const encounterId = ref<number>(props?.encounter?.id || 0)
|
||||
const title = ref('')
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationMeta,
|
||||
searchInput,
|
||||
handlePageChange,
|
||||
handleSearch,
|
||||
fetchData: getMyList,
|
||||
} = usePaginatedList({
|
||||
fetchFn: async ({ page, search }) => {
|
||||
const result = await getList({
|
||||
'encounter-id': route.params.id,
|
||||
includes: 'encounter,employee',
|
||||
'type-code': 'amb-resume',
|
||||
search,
|
||||
page,
|
||||
})
|
||||
if (result.success) {
|
||||
data.value = result.body.data
|
||||
}
|
||||
return { success: result.success || false, body: result.body || {} }
|
||||
},
|
||||
entityName: 'summary-medic',
|
||||
})
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Profil Ringkasan Medis',
|
||||
icon: 'i-lucide-box',
|
||||
refSearchNav: {
|
||||
placeholder: 'Cari (min. 3 karakter)...',
|
||||
minLength: 3,
|
||||
debounceMs: 500,
|
||||
showValidationFeedback: true,
|
||||
onInput: (value: string) => {
|
||||
searchInput.value = value
|
||||
},
|
||||
onClick: () => {},
|
||||
onClear: () => {},
|
||||
},
|
||||
addNav: {
|
||||
label: 'Tambah',
|
||||
icon: 'i-lucide-plus',
|
||||
onClick: () => {
|
||||
goToEntry()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const goEdit = (id: string) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
mode: 'entry',
|
||||
'record-id': id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
|
||||
provide('rec_id', recId)
|
||||
provide('rec_action', recAction)
|
||||
provide('rec_item', recItem)
|
||||
provide('table_data_loader', isLoading)
|
||||
|
||||
const getMyDetail = async (id: number | string) => {
|
||||
const result = await getDetail(id)
|
||||
if (result.success) {
|
||||
const currentValue = result.body?.data || {}
|
||||
recItem.value = currentValue
|
||||
isFormEntryDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for row actions when recId or recAction changes
|
||||
watch(recId, () => {
|
||||
console.log('recId', recId.value)
|
||||
if (recAction.value === ActionEvents.showEdit) {
|
||||
goEdit(recId.value)
|
||||
return
|
||||
} else {
|
||||
isRecordConfirmationOpen.value = true
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await getMyList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header
|
||||
v-model="searchInput"
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="headerPrep.refSearchNav"
|
||||
@search="handleSearch"
|
||||
class="mb-4 xl:mb-5"
|
||||
/>
|
||||
|
||||
<List
|
||||
:data="data"
|
||||
:pagination-meta="paginationMeta"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<!-- Record Confirmation Modal -->
|
||||
<RecordConfirmation
|
||||
v-model:open="isRecordConfirmationOpen"
|
||||
action="delete"
|
||||
:record="recItem"
|
||||
@confirm="() => handleActionRemove(recId, getMyList, toast)"
|
||||
@cancel=""
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.name">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.name }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.code }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<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'
|
||||
import { getDetail } from '~/services/surgery-report.service'
|
||||
|
||||
// Components
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import type { SurgeryReport } from '~/models/surgery-report'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
|
||||
const router = useRouter()
|
||||
const surgeryReport = ref<SurgeryReport | null>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: 'Detail Laporan Operasi',
|
||||
icon: 'i-lucide-newspaper',
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
// onMounted(async () => {
|
||||
// const result = await getDetail(props.record_id, {
|
||||
// includes: "unit,specialist,subspecialist,doctor-employee-person",
|
||||
// })
|
||||
// if (result.success) {
|
||||
// surgeryReport.value = result.body?.data
|
||||
// }
|
||||
// })
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
function handleAction() {
|
||||
goBack()
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
|
||||
<AppSurgeryReportDetail :instance="surgeryReport" @click="handleAction" />
|
||||
</template>
|
||||
@@ -0,0 +1,149 @@
|
||||
<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 Header from '~/components/pub/my-ui/nav-header/prep.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 { getDetail, update } from '~/services/surgery-report.service'
|
||||
import type { SurgeryReport } from '~/models/surgery-report'
|
||||
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
|
||||
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import { withBase } from '~/models/_base'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { SurgeryReportSchema } from '~/schemas/surgery-report.schema'
|
||||
import { formatDateYyyyMmDd } from '~/lib/date'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
import { getList, remove } from '~/services/surgery-report.service'
|
||||
import { handleActionEdit } from '~/handlers/surgery-report.handler'
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
callbackUrl?: string
|
||||
record_id: number
|
||||
}>(), {
|
||||
|
||||
})
|
||||
|
||||
// form related state
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
|
||||
entityName: 'surgery-report',
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region State & Computed
|
||||
const router = useRouter()
|
||||
|
||||
const inputForm = ref<ExposedForm<any> | null>(null)
|
||||
const surgeryReport = ref({})
|
||||
const isConfirmationOpen = ref(false)
|
||||
const selectedOperativeAction = ref<any>(null)
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(async () => {
|
||||
const result = await getDetail(props.record_id)
|
||||
if (result.success) {
|
||||
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
|
||||
surgeryReport.value = responseData
|
||||
inputForm.value?.setValues(responseData)
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
function goBack() {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
async function handleConfirmAdd() {
|
||||
const response = await handleActionEdit(
|
||||
props.record_id,
|
||||
await composeFormData(),
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<SurgeryReport> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = input?.values
|
||||
formData.encounter_id = props.encounter_id
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) {
|
||||
await navigateTo(props.callbackUrl)
|
||||
return
|
||||
}
|
||||
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #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 Laporan Operasi</div>
|
||||
<code>{{ selectedOperativeAction }}</code>
|
||||
<AppSurgeryReportEntry
|
||||
ref="inputForm"
|
||||
:schema="SurgeryReportSchema"
|
||||
:operative-action-list="[]"
|
||||
/>
|
||||
|
||||
<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,193 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
// #region Imports
|
||||
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
|
||||
import { ActionEvents } from '~/components/pub/my-ui/data/types'
|
||||
|
||||
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
|
||||
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
|
||||
import { usePaginatedList } from '~/composables/usePaginatedList'
|
||||
import { getList, remove } from '~/services/surgery-report.service'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import type { Encounter } from '~/models/encounter'
|
||||
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
|
||||
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
|
||||
// #endregion
|
||||
|
||||
// #region State
|
||||
const props = withDefaults(defineProps<{
|
||||
encounter_id: number
|
||||
redirectToForm?: (myRecord_id?: any) => void
|
||||
redirectToDetail?: (myRecord_id: string|number) => void
|
||||
}>(), {
|
||||
redirectToForm: () => { },
|
||||
redirectToDetail: () => { }
|
||||
})
|
||||
|
||||
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
|
||||
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
|
||||
entityName: 'surgery-report',
|
||||
})
|
||||
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
const isFilterDialogOpen = ref(false)
|
||||
const isRecordConfirmationOpen = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
const isRequirementsMet = ref(true)
|
||||
|
||||
const recId = ref<number>(0)
|
||||
const recAction = ref<string>('')
|
||||
const recItem = ref<any>(null)
|
||||
|
||||
const headerPrep: HeaderPrep = {
|
||||
title: "Laporan Operasi",
|
||||
icon: 'i-lucide-history',
|
||||
addNav: {
|
||||
label: "Laporan Operasi",
|
||||
onClick: () => {
|
||||
props.redirectToForm()
|
||||
},
|
||||
},
|
||||
}
|
||||
const refSearchNav: RefSearchNav = {
|
||||
onClick: () => {
|
||||
isFilterDialogOpen.value = true
|
||||
},
|
||||
onInput: (val: string) => {
|
||||
searchInput.value = val
|
||||
},
|
||||
onClear: () => {
|
||||
searchInput.value = ''
|
||||
},
|
||||
}
|
||||
headerPrep.components = [
|
||||
{
|
||||
component: defineAsyncComponent(() => import('~/components/app/surgery-report/_common/btn-history.vue')),
|
||||
props: { }
|
||||
},
|
||||
];
|
||||
// #endregion
|
||||
|
||||
// #region Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
getListData()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// #region Functions
|
||||
async function getListData() {
|
||||
try {
|
||||
summaryLoading.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
console.error('Error fetching Data:', error)
|
||||
} finally {
|
||||
summaryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle confirmation result
|
||||
function handleFiltering(value: any) {
|
||||
isFilterDialogOpen.value = false
|
||||
}
|
||||
async function handleConfirmDelete(record: any, action: string) {
|
||||
if (action === 'delete' && record?.id) {
|
||||
try {
|
||||
const result = await remove(record.id)
|
||||
if (result.success) {
|
||||
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
|
||||
await fetchData()
|
||||
} else {
|
||||
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
provide('isHistoryDialogOpen', isHistoryDialogOpen)
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
watch([recId, recAction], () => {
|
||||
switch (recAction.value) {
|
||||
case ActionEvents.showDetail:
|
||||
props.redirectToDetail(recId.value)
|
||||
break
|
||||
|
||||
case ActionEvents.showEdit:
|
||||
props.redirectToForm(recId.value)
|
||||
break
|
||||
|
||||
case ActionEvents.showConfirmDelete:
|
||||
isRecordConfirmationOpen.value = true
|
||||
break
|
||||
}
|
||||
})
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WarningAlert v-if="!isRequirementsMet"
|
||||
class="mb-5"
|
||||
text="Syarat pembuatan Laporan Operasi belum terpenuhi"
|
||||
:description="[
|
||||
'Lanjutan Penatalaksanaan Pasien harus pulang/KRS.',
|
||||
'Status Resume Medis harus tervalidasi.'
|
||||
]" />
|
||||
|
||||
<div v-else>
|
||||
<Header :prep="headerPrep" />
|
||||
|
||||
<Filter
|
||||
:prep="headerPrep"
|
||||
:ref-search-nav="refSearchNav"
|
||||
:enable-export="false"
|
||||
/>
|
||||
|
||||
<AppSurgeryReportList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
|
||||
|
||||
<Dialog v-model:open="isFilterDialogOpen" title="Filter" size="lg">
|
||||
<AppSurgeryReportCommonFilter @submit="handleFiltering" />
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="isHistoryDialogOpen" title="History">
|
||||
<AppSurgeryReportCommonHistoryDialog />
|
||||
</Dialog>
|
||||
|
||||
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
|
||||
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
|
||||
<template #default="{ record }">
|
||||
<div class="text-sm">
|
||||
<p>
|
||||
<strong>ID:</strong>
|
||||
{{ record?.id }}
|
||||
</p>
|
||||
<p v-if="record?.firstName">
|
||||
<strong>Nama:</strong>
|
||||
{{ record.firstName }}
|
||||
</p>
|
||||
<p v-if="record?.code">
|
||||
<strong>Kode:</strong>
|
||||
{{ record.cellphone }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</RecordConfirmation>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { crudQueryParamsMode } from '~/lib/system-constants';
|
||||
import List from './list.vue'
|
||||
import Detail from './detail.vue'
|
||||
import Entry from './entry.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
encounter_id: number
|
||||
}>()
|
||||
|
||||
const { crudQueryParams, goToDetail, goToEntry } = useQueryCRUD()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<List v-if="crudQueryParams.mode === crudQueryParamsMode.list && !crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:redirectToForm="goToEntry"
|
||||
:redirectToDetail="goToDetail"/>
|
||||
<Detail v-else-if="crudQueryParams.mode === crudQueryParamsMode.list && crudQueryParams.recordId"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId"
|
||||
:redirectToForm="goToEntry"/>
|
||||
<Entry v-else-if="crudQueryParams.mode === crudQueryParamsMode.entry"
|
||||
:encounter_id="encounter_id"
|
||||
:record_id="crudQueryParams.recordId" />
|
||||
|
||||
<List v-else :encounter_id="encounter_id" :redirectToForm="goToEntry" :redirectToDetail="goToDetail" />
|
||||
</template>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { ExposedForm } from '~/types/form'
|
||||
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
|
||||
import { toast } from '~/components/pub/ui/toast'
|
||||
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
|
||||
import { TherapyProtocolMedicRehabilitationSchema, TherapyProtocolSchema } from '~/schemas/therapy-protocol.schema'
|
||||
import { handleActionSave } from '~/handlers/therapy-protocol.handler'
|
||||
|
||||
// #region Props & Emits
|
||||
const props = defineProps<{
|
||||
callbackUrl?: string
|
||||
}>()
|
||||
|
||||
// form related state
|
||||
const route = useRoute()
|
||||
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
|
||||
const inputForm = 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() {
|
||||
const inputData: any = await composeFormData()
|
||||
const response = await handleActionSave(
|
||||
inputData,
|
||||
() => { },
|
||||
() => { },
|
||||
toast,
|
||||
)
|
||||
|
||||
const data = (response?.body?.data ?? null)
|
||||
if (!data) return
|
||||
goBack()
|
||||
}
|
||||
|
||||
async function composeFormData(): Promise<any> {
|
||||
const [input,] = await Promise.all([
|
||||
inputForm.value?.validate(),
|
||||
])
|
||||
|
||||
const results = [input]
|
||||
const allValid = results.every((r) => r?.valid)
|
||||
|
||||
// exit, if form errors happend during validation
|
||||
if (!allValid) return Promise.reject('Form validation failed')
|
||||
|
||||
const formData = input?.values
|
||||
formData.medicalDiagnoses = JSON.stringify(formData.medicalDiagnoses)
|
||||
formData.functionDiagnoses = JSON.stringify(formData.functionDiagnoses)
|
||||
formData.procedures = JSON.stringify(formData.procedures)
|
||||
formData.encounter_id = encounterId
|
||||
|
||||
return new Promise((resolve) => resolve(formData))
|
||||
}
|
||||
// #endregion region
|
||||
|
||||
// #region Utilities & event handlers
|
||||
async function handleActionClick(eventType: string) {
|
||||
if (eventType === 'submit') {
|
||||
isConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'back') {
|
||||
if (props.callbackUrl) return navigateTo(props.callbackUrl)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAdd() {
|
||||
isConfirmationOpen.value = false
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Watchers
|
||||
// #endregion
|
||||
const initial = {
|
||||
// Required String Fields
|
||||
form1ExaminationDate: "2025-11-06",
|
||||
form1Diagnose: 'Awwww',
|
||||
form1TherapyRequest: 'Awwww',
|
||||
form1TargetPeriod: 4,
|
||||
form1TherapyTarget: 'Awwww',
|
||||
form2RelationshipToInsured: 'child',
|
||||
anamnesis: 'Bobo aja wak',
|
||||
form2PhysicalExamination: 'Awwww',
|
||||
medicalDiagnoses: [
|
||||
{
|
||||
id: 3,
|
||||
code: "PROC002",
|
||||
name: "Cesarean section"
|
||||
}
|
||||
],
|
||||
functionDiagnoses: [
|
||||
{
|
||||
id: 3,
|
||||
code: "PROC002",
|
||||
name: "Cesarean section"
|
||||
}
|
||||
],
|
||||
procedures: [
|
||||
{
|
||||
id: 5,
|
||||
code: "PROC0100",
|
||||
name: "Physical therapy session"
|
||||
}
|
||||
],
|
||||
supportingExams: 'Awwww',
|
||||
evaluation: 'Awwww',
|
||||
instruction: 'Awwww',
|
||||
workCauseStatus: 'TIDAK',
|
||||
form3ExaminationDate: "2025-11-06",
|
||||
form3Diagnose: 'Awwww',
|
||||
form3TherapyRequest: 'Awwww',
|
||||
form3ProgramActivities: ['Menonton Kakek Meninggoy',],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah</div>
|
||||
<AppTherapyProtocolEntry
|
||||
ref="inputForm"
|
||||
:schema="TherapyProtocolMedicRehabilitationSchema"
|
||||
:resume-arrangement-type="inputForm?.values.arrangement"
|
||||
:initial-values="initial"
|
||||
/>
|
||||
|
||||
<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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user