initial commit
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
|
||||
import misc404 from '@images/pages/404.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="misc-wrapper">
|
||||
<ErrorHeader
|
||||
error-title="Page Not Found :("
|
||||
error-description="We couldn't find the page you are looking for."
|
||||
/>
|
||||
<VBtn
|
||||
class="mb-12"
|
||||
@click="router.back"
|
||||
>
|
||||
Go Back
|
||||
</VBtn>
|
||||
|
||||
<!-- 👉 Image -->
|
||||
<div class="misc-avatar w-100 text-center">
|
||||
<VImg
|
||||
:src="misc404"
|
||||
alt="Coming Soon"
|
||||
:max-width="200"
|
||||
class="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VImg
|
||||
:src="authThemeMask"
|
||||
class="misc-footer-img d-none d-md-block"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/pages/misc.scss";
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: blank
|
||||
action: read
|
||||
subject: Auth
|
||||
</route>
|
||||
@@ -0,0 +1,301 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const search = ref('')
|
||||
const doctor = ref({})
|
||||
const assignedPatients = ref([])
|
||||
const patients = ref([])
|
||||
|
||||
const unassignedPatients = computed(() => {
|
||||
return patients.value.filter(patient => {
|
||||
return !assignedPatients.value.some(assignedPatient => assignedPatient.id === patient.id)
|
||||
})
|
||||
})
|
||||
|
||||
const searchedAssignedPatients = computed(() => {
|
||||
return assignedPatients.value.filter(patient => {
|
||||
return Object.values(patient).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const searchedUnassignedPatients = computed(() => {
|
||||
return unassignedPatients.value.filter(patient => {
|
||||
return Object.values(patient).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const currentTab = ref(0)
|
||||
|
||||
const details = [
|
||||
{
|
||||
icon: 'tabler-user',
|
||||
title: 'Fullname',
|
||||
key: 'fullname',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-address-book',
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Phone',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Emergency Phone',
|
||||
key: 'emergency_phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-gender-bigender',
|
||||
title: 'Gender',
|
||||
key: 'gender',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-123',
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
},
|
||||
]
|
||||
|
||||
const selectedUnassignPatients = ref([])
|
||||
const selectedAssignPatients = ref([])
|
||||
|
||||
const headers = [
|
||||
{ title: 'Fullname', key: 'fullname' },
|
||||
{ title: 'Phone', key: 'phone' },
|
||||
{ title: 'Gender', key: 'gender' },
|
||||
]
|
||||
|
||||
const sortBy1 = ref([{ key: 'fullname', order: 'asc' }])
|
||||
const sortBy2 = ref([{ key: 'fullname', order: 'asc' }])
|
||||
|
||||
const unassignLoading = ref(false)
|
||||
const assignLoading = ref(false)
|
||||
|
||||
async function unassignPatients() {
|
||||
if (selectedUnassignPatients.value.length === 0) return
|
||||
|
||||
unassignLoading.value = true
|
||||
|
||||
await axiosIns.post('/api/v1/admin/unassign', {
|
||||
doctor_id: route.params.id,
|
||||
patient_ids: selectedUnassignPatients.value,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
|
||||
selectedUnassignPatients.value.forEach(patientId => {
|
||||
const idx = assignedPatients.value.findIndex(patient => patient.id === patientId)
|
||||
|
||||
const toMove = assignedPatients.value[idx]
|
||||
|
||||
assignedPatients.value.splice(idx, 1)
|
||||
patients.value.push(toMove)
|
||||
|
||||
unassignLoading.value = false
|
||||
})
|
||||
|
||||
selectedUnassignPatients.value = []
|
||||
}
|
||||
|
||||
async function assignPatients() {
|
||||
if (selectedAssignPatients.value.length === 0) return
|
||||
|
||||
assignLoading.value = true
|
||||
|
||||
await axiosIns.post('/api/v1/admin/assign', {
|
||||
doctor_id: route.params.id,
|
||||
patient_ids: selectedAssignPatients.value,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
|
||||
selectedAssignPatients.value.forEach(patientId => {
|
||||
const idx = patients.value.findIndex(patient => patient.id === patientId)
|
||||
|
||||
const toMove = patients.value[idx]
|
||||
|
||||
patients.value.splice(idx, 1)
|
||||
assignedPatients.value.push(toMove)
|
||||
})
|
||||
selectedAssignPatients.value = []
|
||||
|
||||
assignLoading.value = false
|
||||
}
|
||||
|
||||
const doctorLoading = ref(false)
|
||||
const patientLoading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
doctorLoading.value = true
|
||||
axiosIns.get(`/api/v1/admin/doctor/${route.params.id}`)
|
||||
.then(response => {
|
||||
doctor.value = response.data.data
|
||||
assignedPatients.value = response.data.data.patients
|
||||
doctorLoading.value = false
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
doctorLoading.value = false
|
||||
})
|
||||
|
||||
patientLoading.value = true
|
||||
axiosIns.get(`/api/v1/admin/patient`)
|
||||
.then(response => {
|
||||
patients.value = response.data.data
|
||||
patientLoading.value = false
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
patientLoading.value = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow>
|
||||
<VCol cols="4">
|
||||
<VCard
|
||||
title="Doctor Details"
|
||||
density="compact"
|
||||
:loading="doctorLoading"
|
||||
>
|
||||
<VCardText>
|
||||
<VList class="text-medium-emphasis">
|
||||
<VListItem
|
||||
v-for="detail in details"
|
||||
:key="detail.key"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="detail.icon"
|
||||
size="32"
|
||||
start
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-light me-1">{{ detail.title }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemMedia>
|
||||
<span class="font-weight-medium me-1">{{ doctor[detail.key] }}</span>
|
||||
</VListItemMedia>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
block
|
||||
@click="router.back"
|
||||
>
|
||||
Back
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VCard
|
||||
title="Patients"
|
||||
density="compact"
|
||||
:loading="patientLoading"
|
||||
>
|
||||
<VTabs v-model="currentTab">
|
||||
<VTab>Assigned Patients</VTab>
|
||||
<VTab>Other Patients</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VWindow v-model="currentTab">
|
||||
<VWindowItem key="0">
|
||||
<VDataTable
|
||||
v-model="selectedUnassignPatients"
|
||||
v-model:sort-by="sortBy1"
|
||||
:headers="headers"
|
||||
:items="searchedAssignedPatients"
|
||||
:items-per-page="10"
|
||||
show-select
|
||||
density="compact"
|
||||
>
|
||||
<template #footer.prepend>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:disabled="selectedUnassignPatients.length === 0 || unassignLoading"
|
||||
:loading="unassignLoading"
|
||||
@click="unassignPatients"
|
||||
>
|
||||
Unassign
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VWindowItem>
|
||||
<VWindowItem key="1">
|
||||
<VDataTable
|
||||
v-model="selectedAssignPatients"
|
||||
v-model:sort-by="sortBy2"
|
||||
:headers="headers"
|
||||
:items="searchedUnassignedPatients"
|
||||
:items-per-page="10"
|
||||
show-select
|
||||
density="compact"
|
||||
>
|
||||
<template #footer.prepend>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:disabled="selectedAssignPatients.length === 0 || assignLoading"
|
||||
:loading="assignLoading"
|
||||
@click="assignPatients"
|
||||
>
|
||||
Assign
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['admin']
|
||||
</route>
|
||||
@@ -0,0 +1,383 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import { emailValidator, requiredValidator } from '@validators'
|
||||
import { onMounted } from 'vue'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const editDialog = ref(false)
|
||||
const deleteDialog = ref(false)
|
||||
|
||||
const refForm = ref()
|
||||
|
||||
const defaultItem = ref({
|
||||
id: '',
|
||||
user_id: '',
|
||||
fullname: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
emergency_phone: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const editedItem = ref(defaultItem.value)
|
||||
const editedIndex = ref(-1)
|
||||
|
||||
const headers = [
|
||||
{ title: 'Fullname', key: 'fullname' },
|
||||
{ title: 'Addres', key: 'address' },
|
||||
{ title: 'Phone', key: 'phone' },
|
||||
{ title: 'Emergency Phone', key: 'emergency_phone' },
|
||||
{ title: 'Gender', key: 'gender' },
|
||||
{ title: 'Age', key: 'age' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'fullname', order: 'asc' }])
|
||||
|
||||
const search = ref('')
|
||||
const doctors = ref([])
|
||||
|
||||
const searchedDoctors = computed(() => {
|
||||
return doctors.value.filter(doctor => {
|
||||
return Object.values(doctor).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const editLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
const editItemShow = item => {
|
||||
editedIndex.value = doctors.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const deleteItemShow = item => {
|
||||
editedIndex.value = doctors.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
deleteDialog.value = true
|
||||
}
|
||||
|
||||
const addItemShow = () => {
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
editDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
email.value = ''
|
||||
password.value = ''
|
||||
refForm.value.reset()
|
||||
}
|
||||
|
||||
const closeDelete = () => {
|
||||
deleteDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
email.value = ''
|
||||
password.value = ''
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const validation = await refForm.value.validate()
|
||||
if (!validation.valid) return
|
||||
|
||||
editLoading.value = true
|
||||
|
||||
if (editedIndex.value > -1) {
|
||||
await axiosIns.put(`/api/v1/admin/doctor/${editedItem.value.id}`, editedItem.value)
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
} else {
|
||||
await axiosIns.post('/api/v1/admin/doctor', {
|
||||
...editedItem.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
}
|
||||
await loadDoctors()
|
||||
editLoading.value = false
|
||||
close()
|
||||
}
|
||||
|
||||
const deleteItemConfirm = async () => {
|
||||
deleteLoading.value = true
|
||||
await axiosIns.delete(`/api/v1/admin/doctor/${editedItem.value.id}`)
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
await loadDoctors()
|
||||
deleteLoading.value = false
|
||||
closeDelete()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const loadDoctors = async () => {
|
||||
loading.value = true
|
||||
await axiosIns.get('/api/v1/admin/doctor')
|
||||
.then(response => {
|
||||
doctors.value = response.data.data
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(loadDoctors)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
title="Doctor"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #append>
|
||||
<div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="addItemShow"
|
||||
>
|
||||
Add Doctor
|
||||
<VIcon
|
||||
end
|
||||
icon="tabler-plus"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
<VCardText density="compact">
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedDoctors"
|
||||
:items-per-page="10"
|
||||
density="compact"
|
||||
>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<RouterLink :to="`/admin/doctor/${item.raw.id}`">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-information-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
</RouterLink>
|
||||
<IconBtn @click="editItemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-pencil-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn @click="deleteItemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-delete-outline"
|
||||
color="error"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- DIALOG EDIT -->
|
||||
<VDialog
|
||||
v-model="editDialog"
|
||||
max-width="600px"
|
||||
persistent
|
||||
>
|
||||
<VCard>
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent="save"
|
||||
>
|
||||
<VCardTitle>
|
||||
<span class="headline">
|
||||
{{ editedIndex > -1 ? "Edit Doctor" : "Add Doctor" }}
|
||||
</span>
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VContainer>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="editedItem.fullname"
|
||||
label="Fullname"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="editedItem.address"
|
||||
label="Address"
|
||||
rows="2"
|
||||
auto-grow
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.phone"
|
||||
label="Phone"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.emergency_phone"
|
||||
label="Emergency Phone"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppSelect
|
||||
v-model="editedItem.gender"
|
||||
:items="['male', 'female']"
|
||||
label="Gender"
|
||||
density="compact"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.age"
|
||||
type="number"
|
||||
label="Age"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- FIELD FOR NEW ITEM -->
|
||||
<VCol
|
||||
v-if="editedIndex == -1"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
:rules="[requiredValidator, emailValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="editedIndex == -1"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:disabled="editLoading"
|
||||
type="button"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
:loading="editLoading"
|
||||
:disabled="editLoading"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VForm>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- DIALOG DELETE -->
|
||||
<VDialog
|
||||
v-model="deleteDialog"
|
||||
max-width="500px"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
Are you sure you want to delete {{ editedItem.fullname }}?
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:disabled="deleteLoading"
|
||||
type="button"
|
||||
@click="closeDelete"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
:loading="deleteLoading"
|
||||
:disabled="deleteLoading"
|
||||
@click="deleteItemConfirm"
|
||||
>
|
||||
Delete
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['admin']
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
class="mb-6"
|
||||
title="Admin Page"
|
||||
>
|
||||
<VCardText>Admin</VCardText>
|
||||
<VCardText>
|
||||
Admin
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['admin']
|
||||
</route>
|
||||
@@ -0,0 +1,207 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const search = ref('')
|
||||
const patient = ref({})
|
||||
const assignedDoctors = ref([])
|
||||
|
||||
const searchedAssignedDoctors = computed(() => {
|
||||
return assignedDoctors.value.filter(doctor => {
|
||||
return Object.values(doctor).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const details = [
|
||||
{
|
||||
icon: 'tabler-user',
|
||||
title: 'Fullname',
|
||||
key: 'fullname',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-address-book',
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Phone',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Emergency Phone',
|
||||
key: 'emergency_phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-gender-bigender',
|
||||
title: 'Gender',
|
||||
key: 'gender',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-123',
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-activity-heartbeat',
|
||||
title: 'Condition',
|
||||
key: 'condition',
|
||||
},
|
||||
]
|
||||
|
||||
const selectedUnassignDoctors = ref([])
|
||||
|
||||
const headers = [
|
||||
{ title: 'Fullname', key: 'fullname' },
|
||||
{ title: 'Phone', key: 'phone' },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'fullname', order: 'asc' }])
|
||||
|
||||
const unassignLoading = ref(false)
|
||||
|
||||
async function unassignDoctors() {
|
||||
if (selectedUnassignDoctors.value.length === 0) return
|
||||
|
||||
unassignLoading.value = true
|
||||
|
||||
await axiosIns.post('/api/v1/admin/unassign/doctor', {
|
||||
patient_id: route.params.id,
|
||||
doctor_ids: selectedUnassignDoctors.value,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
|
||||
selectedUnassignDoctors.value.forEach(doctorId => {
|
||||
const idx = assignedDoctors.value.findIndex(doctor => doctor.id === doctorId)
|
||||
|
||||
assignedDoctors.value.splice(idx, 1)
|
||||
})
|
||||
|
||||
selectedUnassignDoctors.value = []
|
||||
|
||||
unassignLoading.value = false
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
await axiosIns.get(`/api/v1/admin/patient/${route.params.id}`)
|
||||
.then(response => {
|
||||
patient.value = response.data.data
|
||||
assignedDoctors.value = response.data.data.doctors
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow>
|
||||
<VCol cols="4">
|
||||
<VCard
|
||||
title="Patient Details"
|
||||
density="compact"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText>
|
||||
<VList class="text-medium-emphasis">
|
||||
<VListItem
|
||||
v-for="detail in details"
|
||||
:key="detail.key"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="detail.icon"
|
||||
size="32"
|
||||
start
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-light me-1">{{ detail.title }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemMedia>
|
||||
<span class="font-weight-medium me-1">{{ patient[detail.key] }}</span>
|
||||
</VListItemMedia>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
block
|
||||
@click="router.back"
|
||||
>
|
||||
Back
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VCard
|
||||
title="Doctors"
|
||||
density="compact"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model="selectedUnassignDoctors"
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedAssignedDoctors"
|
||||
:items-per-page="10"
|
||||
show-select
|
||||
density="compact"
|
||||
>
|
||||
<template #footer.prepend>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:disabled="selectedUnassignDoctors.length === 0 || unassignLoading"
|
||||
:loading="unassignLoading"
|
||||
@click="unassignDoctors"
|
||||
>
|
||||
Unassign
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['admin']
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import uDiagnose from '@/utils/diagnose'
|
||||
import { emailValidator, requiredValidator } from '@validators'
|
||||
import { onMounted } from 'vue'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const editDialog = ref(false)
|
||||
const deleteDialog = ref(false)
|
||||
|
||||
const refForm = ref()
|
||||
|
||||
const defaultItem = ref({
|
||||
id: '',
|
||||
user_id: '',
|
||||
fullname: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
emergency_phone: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
condition: '',
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const editedItem = ref(defaultItem.value)
|
||||
const editedIndex = ref(-1)
|
||||
|
||||
const headers = [
|
||||
{ title: 'Fullname', key: 'fullname' },
|
||||
{ title: 'Addres', key: 'address' },
|
||||
{ title: 'Phone', key: 'phone' },
|
||||
{ title: 'Emergency Phone', key: 'emergency_phone' },
|
||||
{ title: 'Gender', key: 'gender' },
|
||||
{ title: 'Age', key: 'age' },
|
||||
{ title: 'Condition', key: 'condition' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'fullname', order: 'asc' }])
|
||||
|
||||
const search = ref('')
|
||||
const patients = ref([])
|
||||
|
||||
const searchedPatients = computed(() => {
|
||||
return patients.value.filter(patient => {
|
||||
return Object.values(patient).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const editLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
const editItemShow = item => {
|
||||
editedIndex.value = patients.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const deleteItemShow = item => {
|
||||
editedIndex.value = patients.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
deleteDialog.value = true
|
||||
}
|
||||
|
||||
const addItemShow = () => {
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
editDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
email.value = ''
|
||||
password.value = ''
|
||||
refForm.value.reset()
|
||||
}
|
||||
|
||||
const closeDelete = () => {
|
||||
deleteDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
email.value = ''
|
||||
password.value = ''
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const validation = await refForm.value.validate()
|
||||
if (!validation.valid) return
|
||||
|
||||
editLoading.value = true
|
||||
|
||||
if (editedIndex.value > -1) {
|
||||
await axiosIns.put(`/api/v1/admin/patient/${editedItem.value.id}`, editedItem.value)
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
} else {
|
||||
await axiosIns.post('/api/v1/admin/patient', {
|
||||
...editedItem.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
}
|
||||
await loadPatients()
|
||||
editLoading.value = false
|
||||
close()
|
||||
}
|
||||
|
||||
const deleteItemConfirm = async () => {
|
||||
deleteLoading.value = true
|
||||
await axiosIns.delete(`/api/v1/admin/patient/${editedItem.value.id}`)
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
await loadPatients()
|
||||
deleteLoading.value = false
|
||||
closeDelete()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const loadPatients = async () => {
|
||||
loading.value = true
|
||||
await axiosIns.get('/api/v1/admin/patient')
|
||||
.then(response => {
|
||||
patients.value = response.data.data
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(loadPatients)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
title="Patient"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #append>
|
||||
<div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="addItemShow"
|
||||
>
|
||||
Add Patient
|
||||
<VIcon
|
||||
end
|
||||
icon="tabler-plus"
|
||||
/>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
<VCardText density="compact">
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedPatients"
|
||||
:items-per-page="10"
|
||||
density="compact"
|
||||
>
|
||||
<template #item.condition="{ item }">
|
||||
<VChip
|
||||
:color="uDiagnose.color(item.raw.condition)"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ uDiagnose.text(item.raw.condition) }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<RouterLink :to="`/admin/patient/${item.raw.id}`">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-information-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
</RouterLink>
|
||||
<IconBtn @click="editItemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-pencil-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
<IconBtn @click="deleteItemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-delete-outline"
|
||||
color="error"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- DIALOG EDIT -->
|
||||
<VDialog
|
||||
v-model="editDialog"
|
||||
max-width="600px"
|
||||
persistent
|
||||
>
|
||||
<VCard>
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent="save"
|
||||
>
|
||||
<VCardTitle>
|
||||
<span class="headline">
|
||||
{{ editedIndex > -1 ? "Edit Patient" : "Add Patient" }}
|
||||
</span>
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VContainer>
|
||||
<VRow dense>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="editedItem.fullname"
|
||||
label="Fullname"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="editedItem.address"
|
||||
label="Address"
|
||||
rows="2"
|
||||
auto-grow
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.phone"
|
||||
label="Phone"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.emergency_phone"
|
||||
label="Emergency Phone"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppSelect
|
||||
v-model="editedItem.gender"
|
||||
:items="['male', 'female']"
|
||||
label="Gender"
|
||||
density="compact"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppTextField
|
||||
v-model="editedItem.age"
|
||||
type="number"
|
||||
label="Age"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol :cols="editedIndex == -1 ? '6' : '12'">
|
||||
<AppSelect
|
||||
v-model="editedItem.condition"
|
||||
:items="['normal', 'mi']"
|
||||
label="Condition"
|
||||
density="compact"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<!-- FIELD FOR NEW ITEM -->
|
||||
<VCol
|
||||
v-if="editedIndex == -1"
|
||||
cols="6"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="editedItem.device_id"
|
||||
label="Device ID"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="editedIndex == -1"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
:rules="[requiredValidator, emailValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="editedIndex == -1"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
v-model="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:disabled="editLoading"
|
||||
type="button"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
:loading="editLoading"
|
||||
:disabled="editLoading"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VForm>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- DIALOG DELETE -->
|
||||
<VDialog
|
||||
v-model="deleteDialog"
|
||||
max-width="500px"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
Are you sure you want to delete {{ editedItem.fullname }}?
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:disabled="deleteLoading"
|
||||
@click="closeDelete"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
:loading="deleteLoading"
|
||||
:disabled="deleteLoading"
|
||||
@click="deleteItemConfirm"
|
||||
>
|
||||
Delete
|
||||
</VBtn>
|
||||
|
||||
<VSpacer />
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['admin']
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import DashboardAdmin from '@/views/pages/dashboard/admin.vue'
|
||||
import DashboardDoctor from '@/views/pages/dashboard/doctor.vue'
|
||||
import DashboardPatient from '@/views/pages/dashboard/patient.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (authStore?.user?.role) {
|
||||
case 'admin':
|
||||
return 'tabler-shield-check'
|
||||
case 'doctor':
|
||||
return 'tabler-stethoscope'
|
||||
case 'patient':
|
||||
return 'tabler-user'
|
||||
}
|
||||
|
||||
return 'tabler-user'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<VCard title="Account">
|
||||
<VCardText
|
||||
class="d-flex justify-center align-center"
|
||||
style="min-height: 180px;"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
class="d-flex justify-center align-center"
|
||||
>
|
||||
<VAvatar
|
||||
color="primary"
|
||||
size="64"
|
||||
rounded="sm"
|
||||
>
|
||||
<VIcon
|
||||
:icon="icon"
|
||||
size="48"
|
||||
/>
|
||||
</VAvatar>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
Welcome, {{ authStore?.profile?.fullname || authStore?.user?.email }}
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
You are logged in as <span class="font-weight-medium">{{ authStore?.user?.role }}</span>
|
||||
</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<DashboardAdmin v-if="authStore?.user?.role == 'admin'" />
|
||||
<DashboardDoctor v-if="authStore?.user?.role == 'doctor'" />
|
||||
<DashboardPatient v-if="authStore?.user?.role == 'patient'" />
|
||||
</VRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/libs/apex-chart.scss";
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
</route>
|
||||
@@ -0,0 +1,480 @@
|
||||
<script setup>
|
||||
import Waveform from '@/components/Waveform.vue'
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast, showToastCenter } from '@/plugins/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import uDiagnose from '@/utils/diagnose'
|
||||
import { requiredValidator } from '@validators'
|
||||
import { Dropzone } from 'dropzone'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const search = ref('')
|
||||
const patient = ref({})
|
||||
const diagnoses = ref([])
|
||||
|
||||
const searchedDiagnoses = computed(() => {
|
||||
return diagnoses.value.filter(diagnose => {
|
||||
return Object.values(diagnose).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const editDialog = ref(false)
|
||||
|
||||
const defaultItem = ref({
|
||||
id: '',
|
||||
diagnoses: '',
|
||||
notes: '',
|
||||
is_verified: false,
|
||||
file: '',
|
||||
})
|
||||
|
||||
const editedItem = ref(defaultItem.value)
|
||||
const editedIndex = ref(-1)
|
||||
const refForm = ref()
|
||||
|
||||
const loadingEdit = ref(false)
|
||||
const loadingPredict = ref(false)
|
||||
|
||||
const close = () => {
|
||||
editDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
refForm.value.reset()
|
||||
}
|
||||
|
||||
const editItemShow = item => {
|
||||
editedIndex.value = diagnoses.value.indexOf(item)
|
||||
editedItem.value = {
|
||||
...item,
|
||||
}
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const validation = await refForm.value.validate()
|
||||
if (!validation.valid) return
|
||||
|
||||
loadingEdit.value = true
|
||||
|
||||
if (editedIndex.value > -1) {
|
||||
await axiosIns.put(`/api/v1/patient/diagnoses/${editedItem.value.id}`, {
|
||||
diagnoses: editedItem.value.diagnoses,
|
||||
notes: editedItem.value.notes,
|
||||
is_verified: editedItem.value.is_verified,
|
||||
})
|
||||
.then (response => {
|
||||
showToast(response.data.message, 'success')
|
||||
})
|
||||
.catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
})
|
||||
editedItem.value.updated_at = new Date()
|
||||
Object.assign(diagnoses.value[editedIndex.value], editedItem.value)
|
||||
loadDiagnoses()
|
||||
}
|
||||
loadingEdit.value = false
|
||||
close()
|
||||
}
|
||||
|
||||
const repredict = async () => {
|
||||
loadingPredict.value = true
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const fileData = await axiosIns.get('https://apicta-s3-bucket.s3.ap-southeast-1.amazonaws.com/' + editedItem.value.file, {
|
||||
responseType: 'blob',
|
||||
transformRequest: [(data, headers) => {
|
||||
delete headers.Authorization
|
||||
|
||||
return data
|
||||
}],
|
||||
|
||||
}).then(response => {
|
||||
return new File([response.data], 'audio.wav', { type: 'audio/wav' })
|
||||
}).catch(error => {
|
||||
showToastCenter('Cannot get audio file', 'error')
|
||||
})
|
||||
|
||||
if (!fileData) {
|
||||
loadingPredict.value = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
formData.append('file', fileData)
|
||||
formData.append('patient_id', patient.value.id)
|
||||
|
||||
await axiosIns.post('/api/v1/public/predict', formData)
|
||||
.then (response => {
|
||||
const newDiagnose = response.data.data.result == 0 ? 'normal' : 'mi'
|
||||
if (newDiagnose == editedItem.value.diagnoses) {
|
||||
showToastCenter('No changes', 'info')
|
||||
} else {
|
||||
showToastCenter('Diagnoses changed to ' + uDiagnose.text(newDiagnose), 'success')
|
||||
}
|
||||
editedItem.value.diagnoses = newDiagnose
|
||||
editedItem.value.is_verified = false
|
||||
})
|
||||
.catch(error => {
|
||||
showToastCenter(error.response.data.error, 'error')
|
||||
})
|
||||
|
||||
loadingPredict.value = false
|
||||
}
|
||||
|
||||
const details = [
|
||||
{
|
||||
icon: 'tabler-user',
|
||||
title: 'Fullname',
|
||||
key: 'fullname',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-address-book',
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Phone',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-phone',
|
||||
title: 'Emergency Phone',
|
||||
key: 'emergency_phone',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-gender-bigender',
|
||||
title: 'Gender',
|
||||
key: 'gender',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-123',
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
},
|
||||
{
|
||||
icon: 'tabler-activity-heartbeat',
|
||||
title: 'Condition',
|
||||
key: 'condition',
|
||||
},
|
||||
]
|
||||
|
||||
const headers = [
|
||||
{ title: 'Updated', key: 'updated_at' },
|
||||
{ title: 'Notes', key: 'notes' },
|
||||
{ title: 'Diagnoses', key: 'diagnoses' },
|
||||
{ title: 'Verification', key: 'is_verified' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'updated_at', order: 'desc' }])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadDiagnoses() {
|
||||
loading.value = true
|
||||
await axiosIns.get(`/api/v1/doctor/patients/${route.params.id}`)
|
||||
.then(response => {
|
||||
patient.value = response.data.data
|
||||
diagnoses.value = response.data.data.diagnoses?.map(item => ({
|
||||
...item,
|
||||
is_verified: item.is_verified == 1,
|
||||
}))
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDiagnoses()
|
||||
|
||||
if (route.query?.diagnoses_id) {
|
||||
const diagnosesId = route.query.diagnoses_id
|
||||
const item = diagnoses.value.find(item => item.id === diagnosesId)
|
||||
if (item) {
|
||||
editItemShow(item)
|
||||
} else {
|
||||
router.replace({ query: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
|
||||
return date.toLocaleString(navigator.language)
|
||||
}
|
||||
|
||||
// DROPZONE:START
|
||||
const dropRef = ref(null)
|
||||
|
||||
Dropzone.autoDiscover = false
|
||||
|
||||
onMounted(() => {
|
||||
if(dropRef.value !== null) {
|
||||
new Dropzone(dropRef.value, {
|
||||
url: '/api/v1/doctor/predict',
|
||||
acceptedFiles: "audio/*",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authStore.access_token}`,
|
||||
},
|
||||
init: function() {
|
||||
this.on('sending', function(file, xhr, formData) {
|
||||
if (patient?.value?.id) {
|
||||
formData.append('patient_id', patient?.value?.id)
|
||||
} else {
|
||||
xhr.abort()
|
||||
showToast('Authenticating failed, please try again later', 'error')
|
||||
}
|
||||
})
|
||||
this.on('success', function(file, response) {
|
||||
this.removeFile(file)
|
||||
loadDiagnoses()
|
||||
})
|
||||
this.on('error', function(file, response) {
|
||||
this.removeFile(file)
|
||||
showToast(response?.message, 'error')
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// DROPZONE:END
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VRow class="match-height">
|
||||
<VCol cols="3">
|
||||
<VCard
|
||||
title="Patient Details"
|
||||
density="compact"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText>
|
||||
<VList class="text-medium-emphasis">
|
||||
<VListItem
|
||||
v-for="detail in details"
|
||||
:key="detail.key"
|
||||
density="compact"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="detail.icon"
|
||||
size="32"
|
||||
start
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="font-weight-light me-1">{{ detail.title }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemMedia>
|
||||
<span class="font-weight-medium me-1">{{ patient[detail.key] }}</span>
|
||||
</VListItemMedia>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
block
|
||||
@click="router.back"
|
||||
>
|
||||
Back
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="9">
|
||||
<VCard
|
||||
title="Diagnoses History"
|
||||
density="compact"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedDiagnoses"
|
||||
:items-per-page="10"
|
||||
density="compact"
|
||||
>
|
||||
<template #item.updated_at="{ item }">
|
||||
{{ formatTimestamp(item.raw.updated_at) }}
|
||||
</template>
|
||||
|
||||
<template #item.diagnoses="{ item }">
|
||||
<VChip
|
||||
:color="uDiagnose.color(item.raw.diagnoses)"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ uDiagnose.text(item.raw.diagnoses) }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.is_verified="{ item }">
|
||||
<VChip
|
||||
:color="item.raw.is_verified ? 'success' : 'error'"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ item.raw.is_verified ? 'Verified' : 'Not Verified' }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<IconBtn @click="editItemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-pencil-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="Upload Audio">
|
||||
<VCardText density="compact">
|
||||
<div
|
||||
ref="dropRef"
|
||||
class="dropzone custom-dropzone mt-2"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- DIALOG EDIT -->
|
||||
<VDialog
|
||||
v-model="editDialog"
|
||||
max-width="600px"
|
||||
persistent
|
||||
>
|
||||
<VCard>
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent
|
||||
>
|
||||
<VCardTitle>
|
||||
<span class="headline">
|
||||
Edit Diagnoses
|
||||
</span>
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<Waveform :url="'https://apicta-s3-bucket.s3.ap-southeast-1.amazonaws.com/' + editedItem.file" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
v-model="editedItem.notes"
|
||||
label="Notes"
|
||||
rows="2"
|
||||
auto-grow
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<AppSelect
|
||||
v-model="editedItem.diagnoses"
|
||||
:items="['normal', 'mi']"
|
||||
label="Diagnose"
|
||||
density="compact"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VLabel class="text-body-2">
|
||||
Verification
|
||||
</VLabel>
|
||||
<VSwitch
|
||||
v-model="editedItem.is_verified"
|
||||
color="success"
|
||||
:inset="false"
|
||||
:label="editedItem.is_verified ? 'Verified' : 'Not Verified'"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="loadingPredict"
|
||||
:disabled="loadingPredict || loadingEdit"
|
||||
@click="repredict"
|
||||
>
|
||||
Re-Predict
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
:disabled="loadingPredict || loadingEdit"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
:loading="loadingEdit"
|
||||
:disabled="loadingPredict || loadingEdit"
|
||||
@click="save"
|
||||
>
|
||||
Save
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VForm>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.custom-dropzone {
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
color:rgb(114, 114, 114);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['doctor']
|
||||
</route>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { onMounted } from 'vue'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const headers = [
|
||||
{ title: 'Fullname', key: 'fullname' },
|
||||
{ title: 'Addres', key: 'address' },
|
||||
{ title: 'Phone', key: 'phone' },
|
||||
{ title: 'Emergency Phone', key: 'emergency_phone' },
|
||||
{ title: 'Gender', key: 'gender' },
|
||||
{ title: 'Age', key: 'age' },
|
||||
{ title: 'Condition', key: 'condition' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'fullname', order: 'asc' }])
|
||||
|
||||
const search = ref('')
|
||||
const patients = ref([])
|
||||
|
||||
const searchedPatients = computed(() => {
|
||||
return patients.value.filter(patient => {
|
||||
return Object.values(patient).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
await axiosIns.get('/api/v1/doctor/patients')
|
||||
.then(response => {
|
||||
patients.value = response.data.data
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
title="My Patients"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText density="compact">
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedPatients"
|
||||
:items-per-page="10"
|
||||
density="compact"
|
||||
>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<RouterLink :to="`/doctor/patient/${item.raw.id}`">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-information-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['doctor']
|
||||
</route>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup>
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
|
||||
import loginImg from '@images/login_illustration.png'
|
||||
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isPasswordVisible = ref(false)
|
||||
const refVForm = ref()
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const rememberMe = ref(false)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function login() {
|
||||
const validation = await refVForm.value.validate()
|
||||
if (!validation.valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
axiosIns.post('/api/v1/account/login', {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
}).then(async response => {
|
||||
authStore.saveData(response.data.data.access_token)
|
||||
await authStore.getRoleUser()
|
||||
await authStore.getUser()
|
||||
showToast(response.data.message, 'success')
|
||||
loading.value = false
|
||||
router.push('/dashboard')
|
||||
}).catch(error => {
|
||||
showToast(error.response.data.error, 'error')
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow
|
||||
no-gutters
|
||||
class="auth-wrapper bg-surface"
|
||||
>
|
||||
<VCol
|
||||
md="7"
|
||||
lg="8"
|
||||
class="d-none d-md-flex"
|
||||
>
|
||||
<div class="position-relative bg-primary w-100 ma-0">
|
||||
<div class="d-flex align-center justify-center w-100 h-100">
|
||||
<VImg
|
||||
max-width="505"
|
||||
:src="loginImg"
|
||||
class="auth-illustration rounded-lg"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="5"
|
||||
lg="4"
|
||||
class="auth-card-v2 d-flex align-center justify-center"
|
||||
>
|
||||
<VCard
|
||||
flat
|
||||
:max-width="500"
|
||||
class="mt-12 mt-sm-0 pa-4"
|
||||
>
|
||||
<VCardText>
|
||||
<VNodeRenderer
|
||||
:nodes="themeConfig.app.logo"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<h5 class="text-h5 mb-1">
|
||||
Welcome to <span class="text-capitalize"> {{ themeConfig.app.title }}</span>
|
||||
</h5>
|
||||
|
||||
<p class="mb-0">
|
||||
Please login to your account
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<VAlert
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<p class="text-caption mb-2">
|
||||
for testing purposes, you can use the following credentials:
|
||||
</p>
|
||||
<p class="text-caption mb-2">
|
||||
Email: <strong>admin@mail.com</strong> / Pass: <strong>password</strong>
|
||||
</p>
|
||||
<p class="text-caption mb-2">
|
||||
Email: <strong>doctor1@mail.com</strong> / Pass: <strong>password</strong>
|
||||
</p>
|
||||
<p class="text-caption mb-0">
|
||||
Email: <strong>patient1@mail.com</strong> / Pass: <strong>password</strong>
|
||||
</p>
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<VForm
|
||||
ref="refVForm"
|
||||
@submit.prevent="login"
|
||||
>
|
||||
<VRow>
|
||||
<!-- email -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
autofocus
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- password -->
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="password"
|
||||
label="Password"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
|
||||
<!--
|
||||
<div class="d-flex align-center flex-wrap justify-space-between mt-2 mb-4">
|
||||
<VCheckbox
|
||||
v-model="rememberMe"
|
||||
label="Remember me"
|
||||
/>
|
||||
<a
|
||||
class="text-primary ms-2 mb-1"
|
||||
href="#"
|
||||
>
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
-->
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
block
|
||||
type="submit"
|
||||
:disabled="!email || !password || loading"
|
||||
class="mb-2"
|
||||
:loading="loading"
|
||||
>
|
||||
Login
|
||||
</VBtn>
|
||||
<a
|
||||
href="/"
|
||||
class="text-decoration-underline"
|
||||
>
|
||||
Back to Home
|
||||
</a>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@core-scss/template/pages/page-auth.scss";
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: blank
|
||||
requiresAuth: false
|
||||
</route>
|
||||
@@ -0,0 +1,321 @@
|
||||
<script setup>
|
||||
import Waveform from '@/components/Waveform.vue'
|
||||
import axiosIns from '@/plugins/axios'
|
||||
import { showToast } from '@/plugins/toast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import uDiagnose from '@/utils/diagnose'
|
||||
import { Dropzone } from 'dropzone'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { VDataTable } from 'vuetify/labs/VDataTable'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const defaultItem = ref({
|
||||
id: '',
|
||||
diagnoses: '',
|
||||
notes: '',
|
||||
is_verified: false,
|
||||
file: '',
|
||||
doctor: {
|
||||
fullname: '',
|
||||
},
|
||||
})
|
||||
|
||||
const editDialog = ref(false)
|
||||
const editedItem = ref(defaultItem.value)
|
||||
const editedIndex = ref(-1)
|
||||
|
||||
const close = () => {
|
||||
editDialog.value = false
|
||||
editedIndex.value = -1
|
||||
editedItem.value = { ...defaultItem.value }
|
||||
}
|
||||
|
||||
const itemShow = item => {
|
||||
editedIndex.value = diagnoses.value.indexOf(item)
|
||||
editedItem.value = { ...item }
|
||||
editDialog.value = true
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{ title: 'Updated', key: 'updated_at' },
|
||||
{ title: 'Doctor Name', key: 'doctor.fullname' },
|
||||
{ title: 'Notes', key: 'notes' },
|
||||
{ title: 'Diagnoses', key: 'diagnoses' },
|
||||
{ title: 'Verification', key: 'is_verified' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const sortBy = ref([{ key: 'updated_at', order: 'desc' }])
|
||||
|
||||
const search = ref('')
|
||||
const diagnoses = ref([])
|
||||
|
||||
const searchedDiagnoses = computed(() => {
|
||||
return diagnoses.value.filter(diagnose => {
|
||||
return Object.values(diagnose).some(value => {
|
||||
return String(value).toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadDiagnoses() {
|
||||
loading.value = true
|
||||
await axiosIns.get('/api/v1/patient/diagnoses')
|
||||
.then(response => {
|
||||
diagnoses.value = response.data.data?.map(item => ({
|
||||
...item,
|
||||
is_verified: item.is_verified == 1,
|
||||
}))
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDiagnoses()
|
||||
|
||||
if (route.query?.diagnoses_id) {
|
||||
const diagnosesId = route.query.diagnoses_id
|
||||
const item = diagnoses.value.find(item => item.id === diagnosesId)
|
||||
if (item) {
|
||||
itemShow(item)
|
||||
} else {
|
||||
router.replace({ query: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
|
||||
return date.toLocaleString(navigator.language)
|
||||
}
|
||||
|
||||
// DROPZONE:START
|
||||
const dropRef = ref(null)
|
||||
|
||||
Dropzone.autoDiscover = false
|
||||
|
||||
onMounted(() => {
|
||||
if(dropRef.value !== null) {
|
||||
new Dropzone(dropRef.value, {
|
||||
url: '/api/v1/patient/predict',
|
||||
acceptedFiles: "audio/*",
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authStore.access_token}`,
|
||||
},
|
||||
init: function() {
|
||||
this.on('sending', function(file, xhr, formData) {
|
||||
if (authStore?.profile?.id) {
|
||||
formData.append('patient_id', authStore.profile.id)
|
||||
} else {
|
||||
xhr.abort()
|
||||
showToast('Authenticating failed, please try again later', 'error')
|
||||
}
|
||||
})
|
||||
this.on('success', function(file, response) {
|
||||
this.removeFile(file)
|
||||
loadDiagnoses()
|
||||
})
|
||||
this.on('error', function(file, response) {
|
||||
this.removeFile(file)
|
||||
showToast(response?.message, 'error')
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// DROPZONE:END
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
title="My Diagnoses"
|
||||
:loading="loading"
|
||||
>
|
||||
<VCardText density="compact">
|
||||
<AppTextField
|
||||
v-model="search"
|
||||
prepend-inner-icon="tabler-search"
|
||||
:append-inner-icon="search.length > 0 ? 'tabler-x' : null"
|
||||
placeholder="Search"
|
||||
class="mb-2"
|
||||
@click:append-inner="search=''"
|
||||
/>
|
||||
<VDataTable
|
||||
v-model:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="searchedDiagnoses"
|
||||
:items-per-page="10"
|
||||
density="compact"
|
||||
>
|
||||
<template #item.updated_at="{ item }">
|
||||
{{ formatTimestamp(item.raw.updated_at) }}
|
||||
</template>
|
||||
|
||||
<template #item.diagnoses="{ item }">
|
||||
<VChip
|
||||
:color="uDiagnose.color(item.raw.diagnoses)"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ uDiagnose.text(item.raw.diagnoses) }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.is_verified="{ item }">
|
||||
<VChip
|
||||
:color="item.raw.is_verified ? 'success' : 'error'"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ item.raw.is_verified ? 'Verified' : 'Not Verified' }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<IconBtn @click="itemShow(item.raw)">
|
||||
<VIcon
|
||||
icon="mdi-information-outline"
|
||||
color="primary"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard
|
||||
class="mt-4"
|
||||
title="Upload Audio"
|
||||
>
|
||||
<VCardText density="compact">
|
||||
<div
|
||||
ref="dropRef"
|
||||
class="dropzone custom-dropzone mt-2"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- DIALOG SHOW -->
|
||||
<VDialog
|
||||
v-model="editDialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<VCard>
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent
|
||||
>
|
||||
<VCardTitle>
|
||||
<span class="headline">
|
||||
Diagnoses
|
||||
</span>
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<Waveform :url="'https://apicta-s3-bucket.s3.ap-southeast-1.amazonaws.com/' + editedItem.file" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VLabel class="text-body-2 mb-1">
|
||||
Doctor Name
|
||||
</VLabel>
|
||||
<div class="text-h5">
|
||||
{{ editedItem.doctor.fullname }}
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VLabel class="text-body-2 mb-1">
|
||||
Notes
|
||||
</VLabel>
|
||||
<div class="text-h5">
|
||||
{{ editedItem.notes }}
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VLabel class="text-body-2 mb-1">
|
||||
Diagnoses
|
||||
</VLabel>
|
||||
<div>
|
||||
<VChip
|
||||
:color="uDiagnose.color(editedItem.diagnoses)"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ uDiagnose.text(editedItem.diagnoses) }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VLabel class="text-body-2 mb-1">
|
||||
Verification
|
||||
</VLabel>
|
||||
<div>
|
||||
<VChip
|
||||
:color="editedItem.is_verified ? 'success' : 'error'"
|
||||
label
|
||||
variant="elevated"
|
||||
>
|
||||
{{ editedItem.is_verified ? 'Verified' : 'Not Verified' }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VLabel class="text-body-2 mb-1">
|
||||
Updated
|
||||
</VLabel>
|
||||
<div class="text-h5">
|
||||
{{ formatTimestamp(editedItem.updated_at) }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="close"
|
||||
>
|
||||
Close
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VForm>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.custom-dropzone {
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
color:rgb(114, 114, 114);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
allowedRoles: ['patient']
|
||||
</route>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
const currentTab = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<VTabs v-model="currentTab">
|
||||
<VTab>Profile</VTab>
|
||||
<VTab>Account Settings</VTab>
|
||||
<VTab>Security</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VCardText>
|
||||
<VWindow v-model="currentTab">
|
||||
<VWindowItem key="0">
|
||||
<p>Profile</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, tempore dolorum eaque quibusdam quam nemo harum repellat quod sapiente officiis beatae assumenda illum praesentium cumque?
|
||||
</p>
|
||||
</VWindowItem>
|
||||
<VWindowItem key="1">
|
||||
<p>Account Settings</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, tempore dolorum eaque quibusdam quam nemo harum repellat quod sapiente officiis beatae assumenda illum praesentium cumque?
|
||||
</p>
|
||||
</VWindowItem>
|
||||
<VWindowItem key="2">
|
||||
<p>Security</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, tempore dolorum eaque quibusdam quam nemo harum repellat quod sapiente officiis beatae assumenda illum praesentium cumque?
|
||||
</p>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
requiresAuth: true
|
||||
</route>
|
||||
Reference in New Issue
Block a user