This commit is contained in:
Victor Batischev
2024-02-14 18:02:53 +03:00
commit edbf304790
532 changed files with 24353 additions and 0 deletions

View File

@ -0,0 +1,18 @@
/*---------------- User Styles ------------------------*/
@import 'user/ui/UserAvatar/UserAvatar';
/*---------------- Patient Styles ---------------------*/
@import 'patient/ui/PatientRequest/PatientRequest';
@import 'patient/ui/ProgressBar/ProgressBar';
@import 'patient/ui/PatientSurveyCard/PatientSurveyCard';
@import 'patient/ui/PatientHealthMatrix/PatientHealthMatrix';
@import 'patient/ui/PatientBasicInfo/PatientBasicInfo';
@import 'patient/ui/PatientFilesCard/PatientFilesCard';
@import 'patient/ui/PatientReminders/PatientReminders';
@import 'patient/ui/EditableCard/EditableCard';
@import 'patient/ui/InitialAppointment/InitialAppointment';
@import 'patient/ui/QuestionnaireCard/QuestionnaireCard';
@import 'patient/ui/EmptySurvey/EmptySurvey';
@import 'patient/ui/InitialHealthMatrix/InitialHealthMatrix';
@import 'patient/ui/InitialPurpose/InitialPurpose';
@import 'patient/ui/EditableInput/EditableInput';

3
src/entities/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './user'
export * from './patient'
export * from './medical'

View File

@ -0,0 +1,245 @@
import type { AxiosPromise } from 'axios'
import type {
Patient,
PatientReminder,
THealthMatrix,
HealthMatrixData,
PatientAnalysis,
PatientTreatmentCourse,
} from '@/entities'
import { medicalApi } from '@/shared'
import type {
Medical,
SetSurveyData,
AddReminderData,
TreatmentCourse,
AddTreatmentCourseData,
DeleteAppointmentData,
DeleteTreatmentCourseFromPatientData,
MedicalSurvey,
ViewSurveyAnswersData,
SurveyDetail,
MedicalTest,
AddOrUpdateOptimumData,
MarkerOptimums,
AddOrUpdateAnalysisData,
TreatmentCourseData,
} from '../lib'
/**------------------ Surveys -------------------------- */
export const fetchSurveys = (
search?: MedicalAPI.GET.FetchSurveys.Params,
): MedicalAPI.GET.FetchSurveys.Response =>
medicalApi.get('survey', {
params: { search },
})
export const setSurveysToPatient = (
data: MedicalAPI.PUT.SetSurveysToPatient.Params,
): MedicalAPI.PUT.SetSurveysToPatient.Response =>
medicalApi.put(`customer/${data.customer_id}/survey`, {
survey_ids: data.survey_ids,
})
export const fetchSurveyQuestions = (
params: MedicalAPI.GET.FetchQuestionsOfSurvey.Params,
): MedicalAPI.GET.FetchQuestionsOfSurvey.Response =>
medicalApi.get(`survey/${params}`)
export const viewSurveyAnswers = (
params: MedicalAPI.GET.FetchAnswersOfSurvey.Params,
): MedicalAPI.GET.FetchAnswersOfSurvey.Response =>
medicalApi.get(
`customer/${params.customer_id}/survey/${params.survey_attemp_id}`,
)
/**------------------ Reminder -------------------------- */
export const addReminderToPatient = (
data: MedicalAPI.POST.addReminderToPatient.Params,
): MedicalAPI.POST.addReminderToPatient.Response =>
medicalApi.post('eventReminder', data)
/**------------------ Treatmen Course -------------------------- */
export const fetchTreatmentCourse = (
search?: MedicalAPI.GET.FetchTreatmentCourse.Params,
): MedicalAPI.GET.FetchTreatmentCourse.Response =>
medicalApi.get('treatmentCourse', {
params: { search },
})
export const addTreatmentCourseToPatient = (
data: MedicalAPI.POST.AddTreatmentCourseToPatient.Params,
): MedicalAPI.POST.AddTreatmentCourseToPatient.Response =>
medicalApi.post(`users/${data.user_id}/treatmentCourseUser`, data)
export const addFileToTreatment = (data: {
user_id: number
payload: FormData
}): MedicalAPI.POST.AddTreatmentCourseToPatient.Response =>
medicalApi.post(`users/${data.user_id}/treatmentCourseUser`, data.payload)
export const fetchPatientTreatmentCourse = (
data: MedicalAPI.GET.FetchPatientTreatmentCourse.Params,
): MedicalAPI.GET.FetchPatientTreatmentCourse.Response =>
medicalApi.get(`users/${data}/treatmentCourseUser`)
export const deleteTreatmentCourseFromPatient = (
data: MedicalAPI.DELETE.DeleteTreatmentCourseFromPatient.Params,
): MedicalAPI.DELETE.DeleteTreatmentCourseFromPatient.Response =>
medicalApi.delete(
`users/${data.user_id}/treatmentCourseUser/${data.treatment_course_id}`,
)
export const editTreatmentCourse = (
data: MedicalAPI.PUT.UpdateTreatmentCourse.Params,
): MedicalAPI.PUT.UpdateTreatmentCourse.Response =>
medicalApi.put(
`users/${data.user_id}/treatmentCourseUser/${data.treatment_course_id}`,
data.payload,
)
/**------------------ Appointment -------------------------- */
export const deleteAppointmentFromPatient = (
data: MedicalAPI.DELETE.DeleteAppointmentFromPatient.Params,
): MedicalAPI.DELETE.DeleteAppointmentFromPatient.Response =>
medicalApi.delete(`appointment/${data.appointment}`)
/**------------------ Medical Test -------------------------- */
export const fetchMedicalTests =
(): MedicalAPI.GET.FetchAllMedicalTest.Response =>
medicalApi.get('listMedicalTest')
export const updateCustomOptimum = (
data: MedicalAPI.POST.UpdateCustomOptimum.Params,
): MedicalAPI.POST.UpdateCustomOptimum.Response =>
medicalApi.post('optimalCustom', data)
export const addOrUpdateAnalysis = (
data: MedicalAPI.POST.AddOrUpdateAnalysis.Params,
): MedicalAPI.POST.AddOrUpdateAnalysis.Response =>
medicalApi.post(`users/${data.user_id}/analysis`, data)
/**------------------ Health Matrix -------------------------- */
export const updateHealthMatrixValue = (
data: MedicalAPI.POST.UpdateHealthMatrixValue.Params,
): MedicalAPI.POST.UpdateHealthMatrixValue.Response =>
medicalApi.post(`appointment/${data.appointment_id}/healthMatrix`, data)
export namespace MedicalAPI {
export namespace GET {
export namespace FetchSurveys {
export type Params = string
export type Response = AxiosPromise<{
data: Medical['survey_list']
}>
}
export namespace FetchTreatmentCourse {
export type Params = string
export type Response = AxiosPromise<{
data: TreatmentCourse[]
}>
}
export namespace FetchPatientTreatmentCourse {
export type Params = Patient['id']
export type Response = AxiosPromise<{
data: TreatmentCourse[]
}>
}
export namespace FetchAnswersOfSurvey {
export type Params = ViewSurveyAnswersData
export type Response = AxiosPromise<{
data: SurveyDetail
}>
}
export namespace FetchQuestionsOfSurvey {
export type Params = MedicalSurvey['id']
export type Response = AxiosPromise<{
data: SurveyDetail
}>
}
export namespace FetchAllMedicalTest {
export type Response = AxiosPromise<{
data: MedicalTest[]
}>
}
}
export namespace PUT {
export namespace SetSurveysToPatient {
export type Params = SetSurveyData
export type Response = AxiosPromise<{
data: any
}>
}
export namespace UpdateTreatmentCourse {
export type Params = TreatmentCourseData
export type Response = AxiosPromise<{
data: PatientTreatmentCourse
}>
}
}
export namespace POST {
export namespace addReminderToPatient {
export type Params = AddReminderData
export type Response = AxiosPromise<{
data: PatientReminder
}>
}
export namespace AddTreatmentCourseToPatient {
export type Params = AddTreatmentCourseData
export type Response = AxiosPromise<{
data: any
}>
}
export namespace UpdateCustomOptimum {
export type Params = AddOrUpdateOptimumData
export type Response = AxiosPromise<{
data: MarkerOptimums
}>
}
export namespace AddOrUpdateAnalysis {
export type Params = AddOrUpdateAnalysisData
export type Response = AxiosPromise<{
data: PatientAnalysis
}>
}
export namespace UpdateHealthMatrixValue {
export type Params = HealthMatrixData
export type Response = AxiosPromise<{
data: Maybe<THealthMatrix>
}>
}
}
export namespace DELETE {
export namespace DeleteAppointmentFromPatient {
export type Params = DeleteAppointmentData
export type Response = AxiosPromise<{
data: {
success: boolean
data: boolean
message: null | string
}
}>
}
export namespace DeleteTreatmentCourseFromPatient {
export type Params = DeleteTreatmentCourseFromPatientData
export type Response = AxiosPromise<{
data: any
}>
}
}
}

View File

@ -0,0 +1,3 @@
export * from './api'
export * from './lib'
export * from './model'

View File

@ -0,0 +1,44 @@
import type { MedicalTest, BaseAnalysisOptimumValues } from './types'
export function setTestOptimums(
list: MedicalTest[],
patientInfo: {
age: number
sex: string
},
): BaseAnalysisOptimumValues[] {
return list?.map(x => {
const markesLength = x.markers?.length || 0
let optimums
let i
markers: for (i = 0; i < markesLength; i++) {
optimums = x.markers[i].optimums?.find(y => {
const ages = y.age.split('-').map(Number)
if (ages.length == 2) {
return (
ages[0] <= patientInfo.age &&
patientInfo.age <= ages[1] &&
y.sex == patientInfo.sex
)
} else if (ages.length == 1) {
return (
ages[0] <= patientInfo.age && y.sex == patientInfo.sex
)
} else {
return false
}
})
if (optimums && optimums.age) {
break markers
}
}
return {
type: 'base',
test_id: Number(x.id),
sex: patientInfo.sex,
value: String(optimums?.age),
}
})
}

View File

@ -0,0 +1,2 @@
export * from './types'
export * from './helpers'

View File

@ -0,0 +1,215 @@
import type { Appointments, Patient, PatientAnalysis } from '@/entities'
export type Medical = {
survey_list: MedicalSurvey[]
treatment_courses: TreatmentCourse[]
patient_treatment: TreatmentCourse[]
survey: Maybe<SurveyDetail>
medical_test: MedicalTest[]
}
export type MedicalSurvey = {
id: number
title: string
description: string
questions_count: number | string
}
export type SetSurveyData = {
customer_id: number
survey_ids: number[]
}
export type AddReminderData = {
performer_id: number // user id
datetime: string
type: 'appointment' | 'notice'
text: string
}
export type TreatmentCourse = {
id: number
title: string
duration: number
created_at: string | null
updated_at: string | null
nutrition: string | null
medication: string | null
buds: string | null
analysis_and_research: string | null
comment: string | null
enable: 1 | 0
}
export type SurveyDetail = {
id: number
title: string
description: string
questions: {
id: number
question_text: string
question_type: 'text' | 'checkbox' | 'radio'
survey_id: number
options: {
id: number
option: string
question_id: number
sort: null
is_selected?: boolean
model?: any
}[]
answers?: {
id: number
question_id: number
survey_attempt_id: number
user_id: number
answer_text: string
question_option_id: number | null
}[]
}[]
}
export type MedicalTest = {
id: number
title: string
created_at: null | string
updated_at: null | string
unit: null | string
markers: {
id: number
name: string
list_medical_test_id: number
unit: null | string
created_at: null | string
updated_at: null | string
tip_min: null | string
tip_max: null | string
notice: null | string
optimums: MarkerOptimums[]
optimums_custom: MarkerOptimums[]
head: string[]
result: any[]
}[]
}
// For displaying analysis
export type TestMarkers = {
id: number
name: string
list_medical_test_id: number
unit: null | string
created_at: null | string
updated_at: null | string
tip_min: null | string
tip_max: null | string
notice: null | string
optimums: MarkerOptimums | MarkerOptimums[]
optimums_custom: any[]
result?: PatientAnalysis[]
}
export type MarkerOptimums = {
id: number
marker_id: number
sex: string
age: string
min: number
max: number
created_at: string
updated_at: string
interval?: string
}
export type BaseAnalysisOptimumValues = {
type: 'base' | 'custom'
test_id: MedicalTest['id']
sex: string
value: string
}
export type AddTreatmentCourseData = {
user_id: number
appointment_id: number
treatment_course_id?: number
enabled?: boolean | number
file?: File
}
export type DeleteAppointmentData = {
appointment: Appointments['id']
}
export type DeleteTreatmentCourseFromPatientData = {
user_id: Patient['id']
treatment_course_id: TreatmentCourse['id']
}
export type ViewSurveyAnswersData = {
customer_id: Patient['id']
survey_attemp_id: MedicalSurvey['id']
}
export type AddOrUpdateOptimumData = {
list_medical_test_id: number
marker_id: number
sex: string
age: string
min: number
max?: number
}
export type AddOrUpdateAnalysisData = {
list_medical_test_id: number
result?: number | null
quality: string
date: string
user_id: number
marker_id: number
}
export type HealthMatrixData = {
appointment_id: number
antecedents?: string | null
triggers?: string | null
medmators?: string | null
nutrition?: string | null
sleep?: string | null
movement?: string | null
stress?: string | null
relation?: string | null
assimilation?: string | null
assimilation_color?: string | null
energy?: string | null
energy_color?: string | null
inflammation?: string | null
inflammation_color?: string | null
structure?: string | null
structure_color?: string | null
mental?: string | null
mental_color?: string | null
communications?: string | null
communications_color?: string | null
transport?: string | null
transport_color?: string | null
detoxification?: string | null
detoxification_color?: string | null
circle_mental?: string | null
circle_spiritual?: string | null
circle_emotional?: string | null
}
export type TreatmentCourseData = {
user_id: Patient['id']
treatment_course_id?: TreatmentCourse['id']
payload?:
| {
nutrition?: TreatmentCourse['nutrition']
medication?: TreatmentCourse['medication']
buds?: TreatmentCourse['buds']
analysis_and_research?: TreatmentCourse['analysis_and_research']
comment?: TreatmentCourse['comment']
title?: TreatmentCourse['title']
enable?: TreatmentCourse['enable']
}
| FormData
}

View File

@ -0,0 +1 @@
export * from './medical'

View File

@ -0,0 +1,489 @@
import { defineStore, storeToRefs } from 'pinia'
import { computed, reactive, ref } from 'vue'
import { toast } from 'vue3-toastify'
import {
addReminderToPatient,
addTreatmentCourseToPatient,
fetchMedicalTests,
editTreatmentCourse,
fetchPatientTreatmentCourse,
fetchSurveyQuestions,
fetchSurveys,
fetchTreatmentCourse,
setSurveysToPatient,
setTestOptimums,
updateCustomOptimum,
viewSurveyAnswers,
type PatientAnalysis,
addFileToTreatment,
} from '@/entities'
import { usePatientStore } from '@/entities'
import { Stores, declension } from '@/shared'
import type {
Medical,
SetSurveyData,
AddReminderData,
AddTreatmentCourseData,
ViewSurveyAnswersData,
BaseAnalysisOptimumValues,
MarkerOptimums,
AddOrUpdateOptimumData,
TreatmentCourseData,
} from '../lib'
type MedicalState = BaseState<Maybe<Medical>>
export const useMedicalStore = defineStore(Stores.MEDICAL, () => {
/**
* State
*/
const state: MedicalState = reactive({
data: {
survey_list: [],
treatment_courses: [],
patient_treatment: [],
survey: null,
medical_test: [],
},
loading: false,
})
const currOptimums = ref<BaseAnalysisOptimumValues[]>([])
const { analysisResults, infoForMedicalTest } = storeToRefs(
usePatientStore(),
)
/**
* Getters
*/
const surveyList = computed(() => {
return state.data?.survey_list?.map(x => ({
...x,
questions_count: declension(Number(x.questions_count), [
'вопрос',
'вопроса',
'вопросов',
]),
}))
})
const treatmentCourses = computed(
() =>
state.data?.treatment_courses?.map(x => ({
...x,
duration: declension(x.duration, ['день', 'дня', 'дней']),
})),
)
const patientTreatments = computed(() => {
return (
state.data?.patient_treatment?.map(x => ({
id: x.id,
title: x.title,
})) || []
)
})
const currSurvey = computed(() => state.data?.survey)
const medicalTestList = computed(() => {
if (state.data?.medical_test && state.data.medical_test?.length) {
const newList: any[] = []
state.data.medical_test.forEach((x, idx) => {
const date: Set<string> = new Set()
const markers: any[] = []
x.markers?.forEach(el => {
const result: PatientAnalysis[] = []
let optimums: MarkerOptimums | undefined
let interval: string = ''
if (currOptimums.value[idx]['type'] == 'custom') {
optimums = el.optimums_custom?.find(x => {
return (
String(
currOptimums.value[idx]['value'],
).includes(x.age) &&
currOptimums.value[idx].sex == x.sex
)
})
} else {
optimums = el.optimums?.find(x => {
return (
String(
currOptimums.value[idx]['value'],
).includes(x.age) &&
currOptimums.value[idx].sex == x.sex
)
})
}
analysisResults.value?.forEach(a => {
if (a.marker_id === el.id) {
let analysisState: string = ''
if (a.result) {
if (
optimums?.min &&
optimums?.max &&
!Number.isNaN(optimums?.min) &&
!Number.isNaN(optimums?.max)
) {
if (optimums.min > a.result) {
analysisState = 'down'
} else if (
optimums.min <= a.result &&
a.result <= optimums.max
) {
analysisState = 'normal'
} else {
analysisState = 'up'
}
interval = `${
optimums?.min + '-' + optimums?.max
}`
} else if (optimums?.min) {
if (optimums.min > a.result) {
analysisState = 'down'
} else if (optimums.min == a.result) {
analysisState = 'normal'
} else {
analysisState = 'up'
}
interval = `${optimums?.min}`
}
}
date.add(a.date)
result.push({
...a,
state: analysisState,
})
}
})
markers.push({
id: el.id,
name: el.name,
unit: el.unit,
list_medical_test_id: el.list_medical_test_id,
tip_min: el.tip_min,
tip_max: el.tip_max,
notice: el.notice,
result,
optimums: {
...optimums,
interval,
},
})
})
newList.push({
id: x.id,
title: x.title,
unit: x.unit,
analyze_date: [...date],
markers,
})
})
return newList
} else {
return []
}
})
const hasCustomGetters = (testId: number, markerId: number, age: string) =>
computed(() => {
const medicalTest = state.data?.medical_test.find(
x => x.id == testId,
)
const testMarker = medicalTest?.markers.find(x => x.id == markerId)
return testMarker?.optimums_custom.find(x => x.age == age)
})
const updateCurrOptimumsVal = (optimum: {
value: string
sex: string
type: 'base' | 'custom'
test_id: number
}) => {
currOptimums.value.forEach((el, idx) => {
if (el.test_id == optimum.test_id) {
currOptimums.value[idx] = {
...optimum,
sex:
optimum.sex == 'children'
? infoForMedicalTest.value.sex
: optimum.sex,
}
return
}
})
}
/**
* Actions
*/
const setSurveyList = async (search: string) => {
try {
state.loading = true
const { data } = await fetchSurveys(search)
if (data.data && state.data?.survey_list) {
state.data.survey_list = data.data
}
} catch (e) {
console.log(e)
}
state.loading = false
}
const setTreatmentCourseList = async (search: string) => {
try {
state.loading = true
const { data } = await fetchTreatmentCourse(search)
if (data.data?.length && state.data?.treatment_courses) {
state.data.treatment_courses = data.data
}
} catch (e: any) {
console.log('e ->', e)
}
state.loading = false
}
const setPatientTreatmentCourseList = async (payload: number) => {
try {
state.loading = true
const { data } = await fetchPatientTreatmentCourse(payload)
if (data.data?.length && state.data?.patient_treatment) {
state.data.patient_treatment = data.data
}
} catch (e: any) {
console.log('e ->', e)
}
state.loading = false
}
const addDestinationToPatient = async (payload: AddTreatmentCourseData) => {
try {
state.loading = true
const { data } = await addTreatmentCourseToPatient(payload)
if (data.data) {
usePatientStore().setDataToState(data.data)
}
toast.success('Успешно сохранено!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const addFileToTreatmentCourse = async (payload: any) => {
try {
state.loading = true
const { data } = await addFileToTreatment(payload)
if (data.data) {
usePatientStore().setDataToState(data.data)
}
toast.success('Успешно сохранено!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const addSurveyToPatient = async (payload: SetSurveyData) => {
try {
state.loading = true
const { data } = await setSurveysToPatient(payload)
usePatientStore().setDataToState(data.data)
toast.success('Успешно сохранено!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const addReminder = async (payload: AddReminderData) => {
try {
state.loading = true
const { data } = await addReminderToPatient(payload)
usePatientStore().setReminderToState(data.data)
toast.success('Успешно сохранено!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const viewSurvey = async (survey_id: number) => {
state.loading = true
try {
state.loading = true
const { data } = await fetchSurveyQuestions(survey_id)
if (data.data && state.data) {
state.data.survey = data.data
}
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const viewSurveyResult = async (payload: ViewSurveyAnswersData) => {
try {
state.loading = true
const { data } = await viewSurveyAnswers(payload)
if (data.data && state.data) {
state.data.survey = data.data
state.data.survey['questions'] = data.data.questions.map(x => ({
...x,
options: x.options.map(y => ({
...y,
model: y.is_selected ? y.id : '',
})),
}))
}
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
state.loading = false
}
const setMedicalTest = async () => {
try {
state.loading = true
const { data } = await fetchMedicalTests()
if (data.data && data.data?.length && state.data?.medical_test) {
currOptimums.value = setTestOptimums(
data.data,
infoForMedicalTest.value,
)
state.data.medical_test = data.data
}
} catch (e: any) {
console.log('e ->', e)
}
state.loading = true
}
/**
* только для кастомных оптимумов
* @param {AddOrUpdateOptimumData} payload
*/
const addOrUpdateCustomOptimum = async (
payload: AddOrUpdateOptimumData,
) => {
try {
const { data } = await updateCustomOptimum(payload)
toast.success('Успешно сохранено!')
if (data.data && data.data?.marker_id) {
const testIdx = state.data?.medical_test.length || 0
let markerIdx, optimumIdx, i, j
testLoop: for (i = 0; i < testIdx; i++) {
if (
state.data?.medical_test[i]['id'] ==
payload.list_medical_test_id
) {
markerIdx =
state.data?.medical_test[i]?.markers?.length || 0
for (j = 0; j < markerIdx; j++) {
if (
state.data.medical_test[i]['markers'][j][
'id'
] == payload.marker_id
) {
optimumIdx =
state.data.medical_test[i]['markers'][j]?.[
'optimums_custom'
]?.length || 0
let hasOptimum = false
for (let k = 0; k < optimumIdx; k++) {
if (
state.data.medical_test[i]['markers'][
j
]?.['optimums_custom'][k]['sex'] ==
payload.sex &&
state.data.medical_test[i]['markers'][
j
]?.['optimums_custom'][k]['age'] ==
payload.age
) {
state.data.medical_test[i]['markers'][
j
]['optimums_custom'][k] = {
...data.data,
}
hasOptimum = true
}
}
if (!hasOptimum) {
state.data.medical_test[i]['markers'][j][
'optimums_custom'
].push({
...data.data,
})
break testLoop
}
}
}
}
}
}
} catch (e: any) {
console.log('e ->', e)
}
}
const updateTreatmentCourse = async (payload: TreatmentCourseData) => {
try {
const { data } = await editTreatmentCourse(payload)
if (data.data) {
usePatientStore().setTreatmentDataToState(data.data)
}
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
}
}
return {
state,
surveyList,
currSurvey,
treatmentCourses,
patientTreatments,
medicalTestList,
currOptimums,
hasCustomGetters,
updateCurrOptimumsVal,
setSurveyList,
addSurveyToPatient,
addReminder,
setTreatmentCourseList,
addDestinationToPatient,
setPatientTreatmentCourseList,
viewSurvey,
viewSurveyResult,
setMedicalTest,
addOrUpdateCustomOptimum,
updateTreatmentCourse,
addFileToTreatmentCourse,
}
})

View File

View File

@ -0,0 +1,180 @@
import { type AxiosPromise } from 'axios'
import { baseApi, medicalApi } from '@/shared'
import type {
Appointments,
EditAppointmentData,
EditPatientData,
Patient,
PatientAnalysis,
PatientMediaFile,
} from '../lib'
/**------------------ Patient -------------------------- */
export const fetchPatients = async ({
search,
page,
per_page,
}: PatientAPI.GET.FetchPatients.Params): PatientAPI.GET.FetchPatients.Response => {
const response = await baseApi.get(
`customer?search=${search}&page=${page}&perPage=${per_page}`,
)
return response.data
}
export const fetchPatient = async (
data: PatientAPI.GET.FetchPatient.Params,
): PatientAPI.GET.FetchPatient.Response => {
const response = await baseApi.get(`customer/${data}`)
return response.data
}
export const deletePatient = (data: PatientAPI.DELETE.DeletePatient.Params) =>
new Promise(resolve => {
setTimeout(() => {
console.log('delete: ', data)
resolve(data)
}, 1000)
})
export const editPatient = (
data: PatientAPI.PUT.EditPatient.Params,
): PatientAPI.PUT.EditPatient.Response =>
baseApi.put(`customer/${data.id}`, data)
/**------------------ Patient Appointment -------------------------- */
export const editPatientAppointment = (
data: PatientAPI.PUT.EditAppointment.Params,
): PatientAPI.PUT.EditAppointment.Response =>
medicalApi.put(`appointment/${data.id}`, data)
export const addAppointmentToPatient = (
data: PatientAPI.POST.AddAppointment.Params,
): PatientAPI.POST.AddAppointment.Response =>
medicalApi.post('appointment', data)
/**------------------ Set Avatar of Customer -------------------------- */
export const setPatientAvatar = (
data: PatientAPI.POST.SetAvatar.Params,
): PatientAPI.POST.SetAvatar.Response => baseApi.post('user/setAvatar', data)
/**------------------ Media Files -------------------------- */
export const setFilesToPatient = (
data: PatientAPI.POST.SetFiles.Params,
): PatientAPI.POST.SetFiles.Response =>
medicalApi.post(`customer/${data.customer_id}/file`, data.files)
export const deleteMediaFile = async (
data: PatientAPI.DELETE.DeleteFile.Params,
) => medicalApi.delete(`file/${data}`)
/**------------------ Medical Tests -------------------------- */
export const fetchCustomerMedicalTest = (
customer_id: PatientAPI.GET.FetchCustomerMedicalTests.Params,
): PatientAPI.GET.FetchCustomerMedicalTests.Response =>
medicalApi.get(`users/${customer_id}/analysis`)
/**
* Module PatientAPI
* Interfaces:
* EditPatientParams
* EditAppointmentParams
*/
interface EditPatientParams extends EditPatientData {
id: number
}
interface EditAppointmentParams extends EditAppointmentData {
id: Appointments['id']
user_id: Appointments['user_id']
}
export namespace PatientAPI {
export namespace GET {
export namespace FetchPatients {
export type Params = {
page: number
per_page: number
search: string
}
export type Response = AxiosPromise<PaginationData<Patient[]>>
}
export namespace FetchPatient {
export type Params = Patient['id']
export type Response = AxiosPromise<Patient>
// export type Response = AxiosResponse<{
// data: Patient[]
// }>
}
export namespace FetchCustomerMedicalTests {
export type Params = Patient['id']
export type Response = AxiosPromise<{
data: PatientAnalysis[]
}>
}
}
export namespace DELETE {
export namespace DeletePatient {
export type Params = Patient['id']
}
export namespace DeleteFile {
export type Params = PatientMediaFile['id']
}
}
export namespace PUT {
export namespace EditPatient {
export type Params = EditPatientParams
export type Response = AxiosPromise<{
data: Patient
}>
}
export namespace EditAppointment {
export type Params = EditAppointmentParams
export type Response = AxiosPromise<{
data: Appointments
}>
}
}
export namespace POST {
export namespace AddAppointment {
export type Params = {
user_id: Appointments['user_id']
}
export type Response = AxiosPromise<{
data: {
user_id: number
updated_at: string
created_at: string
id: number
}
}>
}
export namespace SetAvatar {
export type Params = FormData
export type Response = AxiosPromise<{
data: Patient
}>
}
export namespace SetFiles {
export type Params = {
customer_id: Patient['id']
files: FormData
}
export type Response = AxiosPromise<{
data: Patient
}>
}
}
}

View File

@ -0,0 +1,4 @@
export * from './api'
export * from './ui'
export * from './lib'
export * from './model'

View File

@ -0,0 +1,65 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { fetchPatients } from '../api'
import type { Patient, PatientsState, PatientTableRow } from '../lib'
export const useSearchPatient = (cb: (search: string) => void) => {
let timeout: Timeout
const search = ref('')
onBeforeUnmount(() => {
clearTimeout(timeout)
})
watch(search, value => {
clearTimeout(timeout)
timeout = setTimeout(async () => {
await cb(value)
}, 300)
})
return search
}
export const useFetchPatients = async (
search: string = '',
page: Pagination['current_page'] = 1,
state: PatientsState,
converter: (list: Patient[]) => PatientTableRow[],
) => {
const { data } = await fetchPatients({
page,
per_page: state.pagination.per_page,
search,
})
const newData = converter(data.data)
return {
pagination: data,
data: !state.data || search ? newData : [...state.data, ...newData],
}
}
export const useBasePatientsLoad = (
cb: (search?: string, page?: Pagination['current_page']) => Promise<void>,
currentPage: Pagination['current_page'],
lastPage: Pagination['last_page'],
isFirstLoad: boolean,
) => {
const loadData = async ($state: any) => {
await cb('', currentPage + 1)
if (currentPage < lastPage) {
$state.loaded()
} else {
$state.complete()
}
}
onMounted(async () => {
if (isFirstLoad) await cb()
})
return loadData
}

View File

@ -0,0 +1,2 @@
export * from './types'
export * from './hooks'

View File

@ -0,0 +1,205 @@
import type { LinkProps, UserBaseProps } from '@/shared'
import type { PatientRequestProps } from '../ui'
export type Patient = {
id: number
sex: string
avatar: string
name: string
gender: Gender
anamnesis: string | null
asking: string | null
birthdate: string
children_count: number
city: string
contact: string
email: string
marital: string
profession: string
media: PatientMediaFile[]
medical_test: PatientMedicalTest[]
survey_attempts: PatientSurvey[]
files: PatientFiles[]
health_matrix: number[]
appointments: Appointments[]
event_reminder_customer: PatientReminder[]
}
export type PatientTableRow = {
id: Patient['id']
patient: UserBaseProps
age: number
gender: Gender
contact?: LinkProps
time?: string
actions?: boolean
type?: string
description?: string
request?: PatientRequestProps['request']
applicationDate?: string
reminder?: string
}
export type PatientsState = BaseStatePagination<Maybe<PatientTableRow[]>>
export enum PatientStep {
MAIN,
QUESTIONNAIRE,
ANALYZES,
FILES,
HEALTH_MATRIX,
PURPOSE,
}
export type PatientFiles = {
id: number
name: string
file_name: string
url: string
}
export type PatientSurvey = {
id: number
title: string
survey_id: number
attemp: number
percent: number
answers_count: number
survey: {
id: number
title: string
questions_count: number
description: string
}
}
export type PatientMediaFile = {
id: number
file_name: string
created_at: string
}
export type PatientMedicalTest = {
id: number
title: string
created_at: string
unit: string | null
medical_tests: any[]
}
export type Appointments = {
id: number
user_id: number
complaint: string
taking_medication: string
taking_bud: string
physical_activity: string
stress: string
sleep: string
bad_habits: string
conclusion: string
created_at: string
updated_at: string
treatment_course_user?: PatientTreatmentCourse
health_matrix: THealthMatrix
}
export type THealthMatrix = {
id: number
appointment_id: number
antecedents: string | null
triggers: string | null
medmators: string | null
nutrition: string | null
sleep: string | null
movement: string | null
stress: string | null
relation: string | null
assimilation: string | null
assimilation_color: string | null
energy: string | null
energy_color: string | null
inflammation: string | null
inflammation_color: string | null
structure: string | null
structure_color: string | null
mental: string | null
mental_color: string | null
communications: string | null
communications_color: string | null
transport: string | null
transport_color: string | null
detoxification: string | null
detoxification_color: string | null
circle_mental: string | null
circle_spiritual: string | null
circle_emotional: string | null
created_at: string
updated_at: string
}
export type EditPatientData = {
name?: string
sex?: string
city?: string
asking?: string
marital?: string
contact?: string
anamnesis?: string
birthdate?: string
profession?: string
children_count?: string
}
export type EditAppointmentData = {
complaint?: Appointments['complaint']
taking_medication?: Appointments['taking_medication']
taking_bud?: Appointments['taking_bud']
physical_activity?: Appointments['physical_activity']
stress?: Appointments['stress']
sleep?: Appointments['sleep']
bad_habits?: Appointments['bad_habits']
conclusion?: Appointments['conclusion']
}
export type PatientReminder = {
id: number
text: string
type: string
user_id: number
performer_id: number
datetime: string
created_at: string
updated_at: string
}
export type PatientTreatmentCourse = {
id: number
title: string
duration: number | null
user_id: number
performer_id: number
created_at: string
updated_at: string
appointment_id: number
nutrition: string | null
medication: string
buds: string | null
analysis_and_research: string | null
comment: string | null
media: PatientFiles[]
enabled: number
}
export type PatientAnalysis = {
id?: number
user_id: number
performer_id?: number
result: number | null
marker_id: number
quality: string
date: string
created_at?: string
updated_at?: string
state?: string
}

View File

@ -0,0 +1,2 @@
export * from './patientsToMyPatients'
export * from './patientsToRequestsPatients'

View File

@ -0,0 +1,20 @@
import { dateToAge } from '@/shared'
import type { Patient, PatientTableRow } from '../../lib'
export const patientsToMyPatients = (list: Patient[]): PatientTableRow[] => {
return list.map(item => ({
id: item.id,
patient: {
name: item.name || '',
avatar: item.avatar || '',
},
gender: 1,
age: dateToAge(item.birthdate),
request: item.asking || '',
reminder: 'моковые данные, с бека не приходит!!!',
contact: {
href: `tel:${item.contact || ''}`,
text: item.contact || '',
},
}))
}

View File

@ -0,0 +1,19 @@
import { dateToAge } from '@/shared'
import type { Patient, PatientTableRow } from '../../lib'
export const patientsToRequestsPatients = (
list: Patient[],
): PatientTableRow[] => {
return list.map(item => ({
id: item.id,
patient: {
name: item.name || '',
avatar: item.avatar || '',
},
gender: 1,
age: dateToAge(item.birthdate),
applicationDate: '2023-10-23T05:57:37.000000Z',
request: item.asking || '',
actions: true,
}))
}

View File

@ -0,0 +1 @@
export * from './module'

View File

@ -0,0 +1,3 @@
export * from './my-patients'
export * from './request-patients'
export * from './patient'

View File

@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { patientsToMyPatients } from '@/entities/patient/model/converters'
import { Stores } from '@/shared'
import { deletePatient } from '../../api'
import type { Patient, PatientsState } from '../../lib'
import { useFetchPatients } from '../../lib'
export const usePatientsStore = defineStore(Stores.MY_PATIENTS, () => {
const state = reactive<PatientsState>({
loading: false,
data: null,
pagination: {
current_page: 1,
per_page: 10,
last_page: 1,
},
})
const setMyPatients = async (
search: string = '',
page: Pagination['current_page'] = 1,
) => {
try {
const { data, pagination } = await useFetchPatients(
search,
page,
state,
patientsToMyPatients,
)
state.pagination = pagination
state.data = data
} catch (e) {
console.log(e)
}
}
const deleteMyPatient = async (id: Patient['id']) => {
try {
await deletePatient(id)
state.data = state.data?.filter(item => item.id !== id)
} catch (e) {
console.log(e)
}
}
return {
state,
setMyPatients: setMyPatients,
deleteMyPatient: deleteMyPatient,
}
})

View File

@ -0,0 +1,562 @@
import { defineStore } from 'pinia'
import { computed, reactive, ref } from 'vue'
import { toast } from 'vue3-toastify'
import {
editPatient,
fetchPatient,
editPatientAppointment,
addAppointmentToPatient,
setPatientAvatar,
setFilesToPatient,
deleteMediaFile,
deleteAppointmentFromPatient,
deleteTreatmentCourseFromPatient,
updateHealthMatrixValue,
type DeleteTreatmentCourseFromPatientData,
fetchCustomerMedicalTest,
addOrUpdateAnalysis,
type AddOrUpdateAnalysisData,
type HealthMatrixData,
} from '@/entities'
import {
Stores,
dateToAge,
declension,
formattingDateForClient,
getTimeFromDate,
prettifyDate,
} from '@/shared'
import {
PatientStep,
type Appointments,
type EditAppointmentData,
type EditPatientData,
type Patient,
type PatientReminder,
type PatientMediaFile,
type PatientAnalysis,
type PatientTreatmentCourse,
} from '../../lib'
type PatientState = BaseState<Maybe<Patient>>
export const APPOINTMENTSITEMS: {
key:
| 'taking_medication'
| 'taking_bud'
| 'physical_activity'
| 'stress'
| 'sleep'
| 'bad_habits'
| 'complaint'
name: any
}[] = [
{
name: 'Прием медикаментов',
key: 'taking_medication',
},
{
name: 'Прием бадов',
key: 'taking_bud',
},
{
name: 'Физическая активность',
key: 'physical_activity',
},
{
name: 'Стресс',
key: 'stress',
},
{
name: 'Сон',
key: 'sleep',
},
{
name: 'Вредные привычки',
key: 'bad_habits',
},
{
name: 'Жалобы',
key: 'complaint',
},
]
export const usePatientStore = defineStore(Stores.PATIENT, () => {
/**-------- State -------------- */
const state = reactive<PatientState>({
loading: false,
data: null,
})
const analysisResults = ref<PatientAnalysis[]>([])
const idxAppointment = ref(0)
const patientStep = ref<PatientStep>(PatientStep.MAIN)
/**-------- Action -------------- */
const setCurrentPatient = async (id: Patient['id']) => {
try {
state.loading = true
const { data } = await fetchPatient(id)
state.data = data
idxAppointment.value = data?.appointments?.[0]?.id || 0
} catch (e) {
console.log(e)
} finally {
state.loading = false
}
}
const resetCurrentPatient = () => {
state.data = null
}
const onEditPatient = async (payload: EditPatientData) => {
try {
const { data } = await editPatient({
...payload,
id: Number(state.data?.id),
})
state.data = data.data
toast.success('Изменения сохранены !')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
state.loading = false
}
const onEditAppointment = async (payload: EditAppointmentData) => {
try {
const { data } = await editPatientAppointment({
...payload,
id: Number(currAppointment.value?.id),
user_id: Number(currAppointment.value?.user_id),
})
const appointment = data.data
let appointmentID = -1
state.data?.appointments.forEach((el, i) => {
if (el.id == appointment.id) {
appointmentID = i
}
})
if (typeof appointmentID == 'number' && appointmentID > -1) {
APPOINTMENTSITEMS.forEach(elem => {
if (state.data?.appointments[appointmentID]) {
state.data.appointments[appointmentID][elem.key] =
appointment[elem.key]
}
})
}
toast.success('Изменения сохранены!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onCreateAppointment = async (payload: {
user_id: Appointments['user_id']
}) => {
try {
await addAppointmentToPatient(payload)
await setCurrentPatient(payload.user_id)
toast.success('Успешно добавлено новый прием!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onUpdateAvatar = async (payload: FormData) => {
try {
const { data } = await setPatientAvatar(payload)
if (data.data) {
state.data = data.data
}
toast.success('Изменения сохранены')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onUploadFiles = async (payload: FormData) => {
try {
const { data } = await setFilesToPatient({
customer_id: Number(state.data?.id),
files: payload,
})
if (data.data) {
state.data = data.data
}
toast.success('Файлы успешно сохранены')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onDeleteMediaFile = async (payload: {
id: PatientMediaFile['id']
type: 'patient' | 'treatmentCourse'
}) => {
try {
state.loading = true
await deleteMediaFile(payload.id)
if (payload.type == 'patient' && state.data?.media) {
const filteredMedia = state.data.media.filter(
file => file.id !== payload.id,
)
state.data.media = filteredMedia
}
if (payload.type == 'treatmentCourse') {
state.data?.appointments?.forEach((app, idx) => {
if (
app.id == currAppointment.value?.id &&
state.data?.appointments[idx]?.treatment_course_user
) {
const media =
state.data.appointments[
idx
].treatment_course_user?.media.filter(
x => x.id != payload.id,
) || []
state.data.appointments[idx].treatment_course_user = {
analysis_and_research:
currAppointment.value.treatment_course_user
?.analysis_and_research || '',
appointment_id:
currAppointment.value.treatment_course_user
?.appointment_id || 0,
buds:
currAppointment.value.treatment_course_user
?.buds || '[]',
comment:
currAppointment.value.treatment_course_user
?.comment || '',
created_at:
currAppointment.value.treatment_course_user
?.created_at || '',
duration:
currAppointment.value.treatment_course_user
?.duration || 0,
enabled:
currAppointment.value.treatment_course_user
?.enabled || 0,
id:
currAppointment.value.treatment_course_user
?.id || 1,
media: media,
medication:
currAppointment.value.treatment_course_user
?.medication || '[]',
nutrition:
currAppointment.value.treatment_course_user
?.nutrition || '',
performer_id:
currAppointment.value.treatment_course_user
?.performer_id || 1,
title:
currAppointment.value.treatment_course_user
?.title || '',
updated_at:
currAppointment.value.treatment_course_user
?.updated_at || '',
user_id:
currAppointment.value.treatment_course_user
?.user_id || 1,
}
}
})
}
toast.success('Файл удален')
} catch (e) {
console.log(e)
toast.error('Произошла ошибка')
} finally {
state.loading = false
}
}
const onDeleteAppointment = async (id: Appointments['id']) => {
try {
await deleteAppointmentFromPatient({ appointment: id })
if (state.data?.appointments) {
state.data.appointments = state.data?.appointments.filter(
x => x.id != id,
)
}
if (idxAppointment.value == id) {
idxAppointment.value = state.data?.appointments?.[0]?.id || 0
}
toast.success('Прием удален')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onDeleteTreatmentCourse = async (
payload: DeleteTreatmentCourseFromPatientData,
) => {
try {
await deleteTreatmentCourseFromPatient(payload)
state.data?.appointments?.forEach((app, idx) => {
if (
app.id == currAppointment.value?.id &&
state.data?.appointments[idx]?.treatment_course_user
) {
state.data.appointments[idx].treatment_course_user =
undefined
}
})
toast.success('Назначения удален')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const setPatientAnalysis = async () => {
try {
if (state.data?.id) {
const { data } = await fetchCustomerMedicalTest(state.data.id)
if (data.data && Array.isArray(data.data)) {
analysisResults.value = data.data
}
}
} catch (e: any) {
console.log('e -> ', e)
}
}
const updatePatientAnalysisResult = async (
payload: AddOrUpdateAnalysisData,
) => {
try {
const { data } = await addOrUpdateAnalysis(payload)
const lenAnalysis = analysisResults.value.length
let isChanged = false
for (let i = 0; i < lenAnalysis; i++) {
if (
analysisResults.value[i]['marker_id'] ==
payload.marker_id &&
analysisResults.value[i]['date'] == payload.date
) {
analysisResults.value[i]['result'] = Number(
data?.data?.result || payload.result,
)
isChanged = true
break
}
}
if (!isChanged) {
// If analysisResult has not been changed, then this is a new analysis
analysisResults.value.push(data.data)
}
} catch (e: any) {
console.log('e -> ', e)
}
}
const updateAnalysisDate = async (payload: AddOrUpdateAnalysisData) => {
try {
const { data } = await addOrUpdateAnalysis(payload)
const lenAnalysis = analysisResults.value.length
for (let i = 0; i < lenAnalysis; i++) {
if (
analysisResults.value[i]['marker_id'] ==
payload.marker_id &&
analysisResults.value[i]['id'] == data.data.id
) {
analysisResults.value[i] = {
...data.data,
}
break
}
}
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const onUpdateHealthMatrix = async (payload: HealthMatrixData) => {
try {
const { data } = await updateHealthMatrixValue(payload)
if (data.data?.appointment_id) {
for (let i = 0; i < appointmentLen.value; i++) {
if (
state.data?.appointments[i]?.health_matrix
?.appointment_id == data.data.appointment_id
) {
state.data.appointments[i].health_matrix = {
...data.data,
}
}
}
}
toast.success('Изменения сохранены!')
} catch (e: any) {
toast.error(`Что-то не так! ${e?.message || ''}`)
console.log('e -> ', e)
}
}
const setDataToState = (data: Patient) => {
state.data = data
}
const setReminderToState = (data: PatientReminder) => {
state.data?.event_reminder_customer.push(data)
}
const setTreatmentDataToState = (data: PatientTreatmentCourse) => {
state.data?.appointments?.forEach((app, idx) => {
if (
app.id == currAppointment.value?.id &&
state.data?.appointments[idx]
) {
state.data.appointments[idx].treatment_course_user = data
}
})
}
/**-------- Getters -------------- */
const appointmentLen = computed(() => state.data?.appointments?.length || 0)
const patientInfo = computed(() => ({
sex: state.data?.sex || '',
name: state.data?.name || '',
city: state.data?.city || '',
avatar: state.data?.avatar || '',
marital: state.data?.marital || '',
contact: state.data?.contact || '',
birthdate: state.data?.birthdate || '',
profession: state.data?.profession || '',
children_count: state.data?.children_count || '',
birthday: prettifyDate(state.data?.birthdate) || '',
age:
declension(dateToAge(state.data?.birthdate || '0'), [
'год',
'года',
'лет',
]) || '',
asking: state.data?.asking || '',
anamnesis: state.data?.anamnesis || '',
}))
const analyzes = computed(
() =>
state.data?.medical_test?.map(x => ({
...x,
created_at: prettifyDate(x.created_at),
})) || [],
)
const survey = computed(
() =>
state.data?.survey_attempts?.map(x => ({
id: x.id,
title: x.survey?.title,
total: x.survey?.questions_count || 0,
answers: x.answers_count || 0,
percent: x.percent,
})),
)
const files = computed(() => state.data?.files)
const media = computed(() => state.data?.media)
const matrixHealth = computed(() => state.data?.health_matrix)
const reminders = computed(
() =>
state.data?.event_reminder_customer?.map(x => ({
id: x.id,
date: String(
formattingDateForClient(x.datetime, 'short'),
).replace(/\./g, ''),
time: getTimeFromDate(x.datetime),
type: x.type,
name: x.text,
})),
)
const appointments = computed(
() =>
state.data?.appointments?.map(x => ({
id: x.id,
name: prettifyDate(x.created_at) || '',
})),
)
const currAppointment = computed(
() => state.data?.appointments?.find(x => x.id == idxAppointment.value),
)
const infoForMedicalTest = computed(
(): {
sex: string
age: number
} => ({
sex: state.data?.sex || '',
age: dateToAge(state.data?.birthdate || '0'),
}),
)
const treatmentCourse = computed((): PatientTreatmentCourse | null => {
return currAppointment.value?.treatment_course_user || null
})
return {
state,
media,
files,
survey,
analyzes,
reminders,
patientInfo,
patientStep,
matrixHealth,
appointments,
idxAppointment,
currAppointment,
analysisResults,
infoForMedicalTest,
treatmentCourse,
setCurrentPatient,
resetCurrentPatient,
onEditPatient,
onEditAppointment,
onCreateAppointment,
setDataToState,
setReminderToState,
setTreatmentDataToState,
onUpdateAvatar,
onUploadFiles,
onDeleteMediaFile,
onDeleteAppointment,
onDeleteTreatmentCourse,
setPatientAnalysis,
updatePatientAnalysisResult,
updateAnalysisDate,
onUpdateHealthMatrix,
}
})

View File

@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { Stores } from '@/shared'
import { type PatientsState, useFetchPatients } from '../../lib'
import { patientsToRequestsPatients } from '../converters'
export const useRequestPatientsStore = defineStore(
Stores.REQUEST_PATIENTS,
() => {
const state = reactive<PatientsState>({
loading: false,
data: null,
pagination: {
current_page: 1,
per_page: 10,
last_page: 0,
},
})
const setRequestPatients = async (
search: string = '',
page: Pagination['current_page'] = 1,
) => {
try {
const { data, pagination } = await useFetchPatients(
search,
page,
state,
patientsToRequestsPatients,
)
state.pagination = pagination
state.data = data
} catch (e) {
console.log(e)
}
}
return {
state,
setRequestPatients,
}
},
)

View File

@ -0,0 +1,58 @@
.editable__card {
&--title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
.card {
gap: toRem(24);
}
pre {
font-family: $mainFontFamily;
color: var(--dark-main);
@include fontSize(
s-13,
(
line-height: 1.3,
)
);
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
p {
color: var(--dark-64);
@include fontSize(
s-13,
(
line-height: 1.3,
)
);
}
textarea {
border: none;
overflow: auto;
outline: none;
@include fontSize(
s-13,
(
line-height: 1.3,
)
);
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
resize: none;
color: var(--dark-main);
}
}

View File

@ -0,0 +1,92 @@
<template>
<div
:class="bem('card')"
@focus="cardFocusEvent"
@blur="cardBlurEvent"
tabindex="0"
>
<card-component rounded hoverable size="m" :active="isFocused">
<template #header>
<h4 :class="bem('card--title')">{{ title }}</h4>
</template>
<template v-if="isFocused">
<textarea
ref="textarea"
v-model="data"
@blur="textareaBlurEvent"
@focus="autoGrow"
@input="autoGrow"
>
</textarea>
</template>
<template v-else>
<pre v-if="value">{{ value }}</pre>
<p v-else>Не заполнено</p>
</template>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="Editable">
import { ref, nextTick, watch } from 'vue'
export type EditableCardProps = {
value: string
title: string
}
const props = defineProps<EditableCardProps>()
const emit = defineEmits<{
(e: 'save', val: string): void
}>()
const isFocused = ref<boolean>(false)
const data = ref<string>(props.value)
const textarea = ref<HTMLElement>()
watch(props, () => {
data.value = props.value
})
/**------------- Methods --------------- */
const cardFocusEvent = (e: FocusEvent) => {
const target = e.relatedTarget as HTMLElement
if (isFocused.value && target?.tagName == 'TEXTAREA') {
return
}
isFocused.value = true
nextTick(() => {
const textarea = document.getElementsByTagName('textarea')?.[0]
textarea?.focus()
})
}
const cardBlurEvent = (e: FocusEvent) => {
const target = e.relatedTarget as HTMLElement
if (target?.tagName !== 'TEXTAREA') {
isFocused.value = false
if (props.value != data.value) emit('save', data.value)
}
}
const textareaBlurEvent = (e: FocusEvent) => {
const target = e.relatedTarget as HTMLElement
if (target?.className !== 'patient-asking__card') {
isFocused.value = false
if (props.value != data.value) emit('save', data.value)
}
}
const autoGrow = () => {
if (textarea.value) {
const height = textarea.value.scrollHeight
if (!data.value) {
textarea.value.style.height = '17px'
} else {
textarea.value.style.height = 'auto'
textarea.value.style.height = height + 'px'
}
}
}
</script>

View File

@ -0,0 +1,3 @@
import EditableCard, { type EditableCardProps } from './EditableCard.vue'
export { EditableCard, type EditableCardProps }

View File

@ -0,0 +1,20 @@
.editable-input {
display: flex;
align-items: center;
p {
@include fontSize(s-13);
cursor: pointer;
padding: toRem(8) 0;
&.empty {
text-align: center;
color: var(--dark-32);
}
}
input {
width: 100%;
padding: toRem(6);
border: 1px solid var(--brand-main);
border-radius: $borderRadius6;
}
}

View File

@ -0,0 +1,43 @@
<template>
<div :class="bem()" tabindex="0" @focus="focusInput">
<template v-if="isFocused">
<input type="text" v-model="val" @blur="blurInput" />
</template>
<template v-else>
<p v-if="modelValue">{{ modelValue }}</p>
<p v-else class="empty">{{ placeholder }}</p>
</template>
</div>
</template>
<script setup lang="ts" bem-block="EditableInput">
import { nextTick, ref } from 'vue'
export type EditableInputProps = {
modelValue: any
placeholder: string
}
const emit = defineEmits(['update:modelValue'])
const props = defineProps<EditableInputProps>()
const isFocused = ref<boolean>()
const val = ref<string>('')
const focusInput = () => {
isFocused.value = true
val.value = props.modelValue
nextTick(() => {
const input = document.querySelector(
'.editable-input input',
) as HTMLInputElement
input?.focus()
})
}
const blurInput = () => {
emit('update:modelValue', val.value)
isFocused.value = false
}
</script>

View File

@ -0,0 +1,3 @@
import EditableInput, { type EditableInputProps } from './EditableInput.vue'
export { EditableInput, type EditableInputProps }

View File

@ -0,0 +1,20 @@
.empty-survey {
aspect-ratio: 1 / 1.1;
max-height: 252px;
background: var(--brand-4-bg);
border-radius: $borderRadius20;
&__title {
@include fontSize(
h3,
(
weight: 500,
)
);
}
&__subtitle {
@include fontSize(b-14);
text-align: center;
}
}

View File

@ -0,0 +1,11 @@
<template>
<div :class="[...bem(), 'column', 'items-center', 'justify-center']">
<span :class="bem('title')">Не назначен ни один опросник</span>
<span :class="bem('subtitle')">
Выберите опросник из библиотеки справа и назначьте пациенту
</span>
</div>
</template>
<script setup lang="ts" bem-block="EmptySurvey">
export type EmptySurveyProps = {}
</script>

View File

@ -0,0 +1,3 @@
import EmptySurvey, { type EmptySurveyProps } from './EmptySurvey.vue'
export { EmptySurvey, type EmptySurveyProps }

View File

@ -0,0 +1,28 @@
.initial-appointment {
aspect-ratio: 1 / 1.1;
max-height: 390px;
background: var(--brand-4-bg);
border-radius: $borderRadius20;
&__title {
@include fontSize(
h2,
(
weight: 500,
)
);
}
&__subtitle {
max-width: 330px;
@include fontSize(b-14);
text-align: center;
}
&__actions {
margin-top: toRem(24);
display: flex;
flex-wrap: wrap;
gap: 12px;
}
}

View File

@ -0,0 +1,27 @@
<template>
<div :class="[...bem(), 'column', 'items-center', 'justify-center']">
<span :class="bem('title')">Первичный прием</span>
<span :class="bem('subtitle')"
>Добавьте карточку первичного приема, чтобы начать собирать данные о
пациенте в динамике</span
>
<div :class="bem('actions')">
<button-component
text="Создать первичный прием"
view="flat"
rounded
size="m"
@click="onCreateAppointment({ user_id })"
/>
</div>
</div>
</template>
<script setup lang="ts" bem-block="InitialAppointment">
import { useRoute } from 'vue-router'
import { usePatientStore } from '@/entities'
export type InitialAppointmentProps = {}
const user_id = Number(useRoute().params.id)
const { onCreateAppointment } = usePatientStore()
</script>

View File

@ -0,0 +1,5 @@
import InitialAppointment, {
type InitialAppointmentProps,
} from './InitialAppointment.vue'
export { InitialAppointment, type InitialAppointmentProps }

View File

@ -0,0 +1,27 @@
.initial-health-matrix {
width: 100%;
aspect-ratio: 1 / 1.1;
max-height: 413px;
background: var(--brand-4-bg);
border-radius: $borderRadius20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: toRem(45);
&__title {
max-width: toRem(340);
text-align: center;
margin-bottom: toRem(24);
@include fontSize(
h3,
(
weight: 700,
)
);
}
}

View File

@ -0,0 +1,23 @@
<template>
<div :class="bem()">
<span :class="bem('title')">
Для добавления матрицы здоровья создайте первый прием
</span>
<button-component
text="Создать прием"
view="brand"
rounded
size="m"
@click="onCreateAppointment({ user_id })"
/>
</div>
</template>
<script setup lang="ts" bem-block="InitialHealthMatrix">
import { useRoute } from 'vue-router'
import { usePatientStore } from '@/entities'
export type InitialHealthMatrixProps = {}
const user_id = Number(useRoute().params.id)
const { onCreateAppointment } = usePatientStore()
</script>

View File

@ -0,0 +1,5 @@
import InitialHealthMatrix, {
type InitialHealthMatrixProps,
} from './InitialHealthMatrix.vue'
export { InitialHealthMatrix, type InitialHealthMatrixProps }

View File

@ -0,0 +1,24 @@
.initial-purpose {
width: 100%;
aspect-ratio: 1 / 1.1;
max-height: 413px;
background: var(--brand-4-bg);
border-radius: $borderRadius20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: toRem(45);
&__title {
max-width: toRem(340);
text-align: center;
margin-bottom: toRem(24);
@include fontSize(
h3,
(
weight: 700,
)
);
}
}

View File

@ -0,0 +1,27 @@
<template>
<div :class="bem()">
<span :class="bem('title')">
Для добавления назначения создайте первый прием
</span>
<button-component
text="Создать прием"
view="brand"
rounded
size="m"
@click="onCreateAppointment({ user_id })"
/>
</div>
</template>
<script setup lang="ts" bem-block="InitialPurpose">
import { useRoute } from 'vue-router'
import { usePatientStore } from '@/entities'
export type InitialPurposeProps = {}
const user_id = Number(useRoute().params.id)
const { onCreateAppointment } = usePatientStore()
</script>

View File

@ -0,0 +1,3 @@
import InitialPurpose, { type InitialPurposeProps } from './InitialPurpose.vue'
export { InitialPurpose, type InitialPurposeProps }

View File

@ -0,0 +1,21 @@
.patient-basic-info {
.row-name {
text-align: left;
font-size: toRem(16);
margin-bottom: 2px;
}
&__row[class$='unavailable'] {
color: var(--dark-32);
}
table {
td,
th {
font-size: toRem(13);
line-height: toRem(20);
padding: toRem(4) 0;
vertical-align: middle;
}
}
}

View File

@ -0,0 +1,133 @@
<template>
<div :class="bem()">
<card-component rounded size="m">
<template #header>
<div class="row justify-between items-center">
<user-avatar
:avatar="patientInfo.avatar"
:user-id="userId"
@update:avatar="updateAvatar"
/>
<button-component
icon="pencil-line"
view="secondary"
size="m"
@click="showEditPatientInfoModal"
/>
</div>
</template>
<table>
<tr v-for="(item, key) in data" :key="key">
<th v-if="item.title" align="left">
{{ item.title }}
</th>
<td
align="right"
:class="
bem(`row row-${key}`, { unavailable: !item.value })
"
>
<span v-if="item.value">{{ item.value }}</span>
<span v-else>{{
item?.placeholder || 'не указано'
}}</span>
</td>
</tr>
</table>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="PatientBasicInfo">
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { ModalsName, useModalsStore } from '@/widgets'
import { usePatientStore, type EditPatientData, UserAvatar } from '@/entities'
export type PatientBasicInfoProps = {}
type DataType = {
[key: string]: {
title: string
value: any
placeholder?: string
}
}
const { patientInfo } = storeToRefs(usePatientStore())
const data = computed<DataType>(() => ({
name: {
title: '',
value: patientInfo.value.name,
placeholder: 'ФИО',
},
sex: {
title: 'Пол',
value: patientInfo.value.sex == 'male' ? 'Мужчина' : 'Женщина',
},
age: {
title: 'Возраст',
value: patientInfo.value.age,
},
birthday: {
title: 'Дата рождения',
value: patientInfo.value.birthday,
},
city: {
title: 'Город проживания',
value: patientInfo.value.city,
},
marital: {
title: 'Семейное положение',
value: patientInfo.value.marital,
},
children_count: {
title: 'Кол-во детей',
value: patientInfo.value.children_count,
},
profession: {
title: 'Профессия',
value: patientInfo.value.profession,
},
contact: {
title: 'Контакты',
value: patientInfo.value.contact,
},
}))
const userId = useRoute().params.id as string
/**------------ Methods ------------------ */
const { onEditPatient, onUpdateAvatar } = usePatientStore()
const modals = useModalsStore()
const updateAvatar = async (formdata: FormData) => {
await onUpdateAvatar(formdata)
}
const savePatientInfo = async (data: EditPatientData) => {
modals.closeModal()
await onEditPatient(data)
}
const showEditPatientInfoModal = () => {
modals.setModal(ModalsName.EDITPATIENT, {
title: 'Основная информация',
data: {
name: patientInfo.value.name,
sex: patientInfo.value.sex,
city: patientInfo.value.city,
marital: patientInfo.value.marital,
contact: patientInfo.value.contact,
birthdate: patientInfo.value.birthdate,
profession: patientInfo.value.profession,
children_count: patientInfo.value.children_count,
},
success: {
text: 'Сохранить',
cb: savePatientInfo,
},
cansel: {
text: 'Отмена',
cb: modals.closeModal,
},
})
}
</script>

View File

@ -0,0 +1,5 @@
import PatientBasicInfo, {
type PatientBasicInfoProps,
} from './PatientBasicInfo.vue'
export { PatientBasicInfo, type PatientBasicInfoProps }

View File

@ -0,0 +1,64 @@
.patient-files {
&__card {
&--title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
&--button {
height: 20px;
font-size: toRem(13);
padding: 0;
&:hover {
background-color: white;
}
}
.card {
gap: toRem(20);
}
p {
font-size: toRem(13);
color: var(--dark-64);
}
}
&__list {
padding: 0 !important;
display: flex;
flex-direction: column;
gap: toRem(10);
&--item {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
.file {
display: flex;
align-items: flex-start;
@include fontSize(s-13);
color: var(--dark-main) !important;
i {
padding: 0 toRem(8) 0 0;
}
&-name {
width: 240px;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
border: none !important;
}
}
}
}
}

View File

@ -0,0 +1,54 @@
<template>
<div :class="bem('card')">
<card-component rounded size="m">
<template #header>
<div class="row justify-between items-center">
<h4 :class="bem('card--title')">Файлы</h4>
<slot name="add-file"></slot>
</div>
</template>
<ul v-if="files?.length" :class="bem('list')">
<li
v-for="(f, i) in files"
:key="`file-${i}`"
:class="bem('list--item')"
>
<a
:href="f.url"
:class="bem(`list--item file`)"
target="_blank"
>
<icon-base-component name="file" size="m" />
<span :class="bem(`list--item file-name`)">{{
f.file_name
}}</span>
</a>
</li>
</ul>
<p v-else>Не ни одного файлв</p>
<template v-if="files?.length" #actions>
<button-component
text="Посмотреть все"
view="secondary"
text-position="left"
:class="bem('card--button')"
@click="moveToFilesStep"
/>
</template>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="PatientFiles">
import { storeToRefs } from 'pinia'
import { usePatientStore, PatientStep } from '@/entities'
export type PatientFilesCardProps = {}
const { files, patientStep } = storeToRefs(usePatientStore())
const moveToFilesStep = () => {
patientStep.value = PatientStep.FILES
}
</script>

View File

@ -0,0 +1,5 @@
import PatientFilesCard, {
type PatientFilesCardProps,
} from './PatientFilesCard.vue'
export { PatientFilesCard, type PatientFilesCardProps }

View File

@ -0,0 +1,51 @@
.health-matrix {
&__card {
&--title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
.card {
gap: toRem(20);
}
}
&__graph {
display: flex;
width: 100%;
overflow: hidden;
&--item {
display: flex;
flex-direction: column;
&:first-child .overlay {
border-radius: toRem(24) 0 0 toRem(24);
}
&:last-child .overlay {
border-radius: 0 toRem(24) toRem(24) 0;
}
}
.overlay {
height: toRem(24);
}
.value {
display: block;
padding: toRem(6) 0 0;
text-align: center;
@include fontSize(
s-13,
(
line-height: toRem(20),
)
);
}
}
}

View File

@ -0,0 +1,54 @@
<template>
<div v-if="matrixHealth?.length" :class="bem('card')">
<card-component rounded size="m">
<template #header>
<div class="row justify-between items-center">
<h4 :class="bem('card--title')">Матрица здоровья</h4>
</div>
</template>
<div :class="bem('graph')">
<div
v-for="(m, i) in matrixHealth"
:key="i"
:class="bem('graph', 'item')"
:style="`flex: 0 0 ${m}%;`"
>
<div
class="overlay"
:style="`background: ${getMatrixColor(i)}`"
></div>
<span class="value">{{ m }}%</span>
</div>
</div>
<template #actions>
<button-component
text="Подробнее "
view="secondary"
size="m"
width="88px"
:class="bem('card--button')"
@click="moveToHealthMatrix"
/>
</template>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="healthMatrix">
import { storeToRefs } from 'pinia'
import { usePatientStore, PatientStep } from '@/entities'
export type PatientHealthMatrixProps = {}
const colors = ['#f25c80', '#fe9b7d', '#b2d677']
const { matrixHealth, patientStep } = storeToRefs(usePatientStore())
const getMatrixColor = (idx: number): string => {
if (matrixHealth.value?.[idx]) return colors?.[idx]
return '#' + Math.floor(Math.random() * 16777215).toString(16)
}
const moveToHealthMatrix = () => {
patientStep.value = PatientStep.HEALTH_MATRIX
}
</script>

View File

@ -0,0 +1,5 @@
import PatientHealthMatrix, {
type PatientHealthMatrixProps,
} from './PatientHealthMatrix.vue'
export { PatientHealthMatrix, type PatientHealthMatrixProps }

View File

@ -0,0 +1,52 @@
<template>
<tabs-component v-model="valueModel" :list="list" />
</template>
<script setup lang="ts" bem-block="PatientNavigation">
import { computed } from 'vue'
import type { TabsProps } from '@/shared'
import { PatientStep } from '../../lib'
export type PatientNavigationProps = {
modelValue: PatientStep
}
type Emits = {
(e: 'update:modelValue', value: PatientStep): PatientStep
}
const props = defineProps<PatientNavigationProps>()
const emit = defineEmits<Emits>()
const valueModel = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
})
const list: TabsProps['list'] = [
{
id: PatientStep.MAIN,
text: 'Карточка',
},
{
id: PatientStep.QUESTIONNAIRE,
text: 'Опросники',
},
{
id: PatientStep.ANALYZES,
text: 'Анализы',
},
{
id: PatientStep.FILES,
text: 'Файлы',
},
{
id: PatientStep.HEALTH_MATRIX,
text: 'Матрица здоровья',
},
{
id: PatientStep.PURPOSE,
text: 'Назначение',
},
]
</script>

View File

@ -0,0 +1,5 @@
import PatientNavigation, {
type PatientNavigationProps,
} from './PatientNavigation.vue'
export { PatientNavigation, type PatientNavigationProps }

View File

@ -0,0 +1,69 @@
.patient-reminders {
&__card {
&--title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
&--button {
height: 20px;
font-size: toRem(13);
padding: 0;
&:hover {
background-color: white;
}
}
.card {
gap: toRem(20);
}
p {
font-size: toRem(13);
color: var(--dark-64);
}
}
&__list {
td,
th {
padding: toRem(8) 0;
@include fontSize(
s-13,
(
line-height: 1.5,
)
);
}
th {
padding-right: toRem(4);
}
span {
min-width: toRem(50);
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--dark-64);
}
mark {
min-width: toRem(50);
height: 20px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 toRem(4);
color: var(--dark-main);
background: var(--blue-20);
border-radius: 3px;
@include fontSize(s-13);
}
}
}

View File

@ -0,0 +1,83 @@
<template>
<div :class="bem('card')">
<card-component rounded size="m">
<template #header>
<div class="row justify-between items-center">
<h4 :class="bem('card--title')">Напоминания</h4>
<button-component
icon="plus"
view="secondary"
size="xs"
@click="showAddReminderModal"
/>
</div>
</template>
<table v-if="reminders?.length" :class="bem('list')">
<tr
v-for="(r, i) in reminders"
:key="`reminder_${i}`"
:class="bem('list--item')"
>
<th align="left">{{ r.name || '--' }}</th>
<td align="right">
<span>{{ r.date }}</span>
<mark>{{ r.time }}</mark>
</td>
</tr>
</table>
<p v-else>Не ни одного напоминания</p>
<template v-if="reminders?.length" #actions>
<button-component
text="Посмотреть все"
view="secondary"
text-position="left"
:class="bem('card--button')"
@click="moveToPurposeStep"
/>
</template>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="PatientReminders">
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { ModalsName, useModalsStore } from '@/widgets'
import { usePatientStore, useMedicalStore, PatientStep } from '@/entities'
export type PatientRemindersProps = {}
const { reminders, patientStep } = storeToRefs(usePatientStore())
const modals = useModalsStore()
const { addReminder } = useMedicalStore()
const performerId = useRoute().params.id
const onAddReminder = async (data: {
datetime: string
type: 'appointment' | 'notice'
text: string
}) => {
await addReminder({
...data,
performer_id: Number(performerId),
})
modals.closeModal()
}
const showAddReminderModal = () => {
modals.setModal(ModalsName.ADDREMINDER, {
title: 'Создать напоминание',
success: {
text: 'Сохранить',
cb: onAddReminder,
},
cansel: {
text: 'Отмена',
cb: modals.closeModal,
},
})
}
const moveToPurposeStep = () => {
patientStep.value = PatientStep.PURPOSE
}
</script>

View File

@ -0,0 +1,5 @@
import PatientReminders, {
type PatientRemindersProps,
} from './PatientReminders.vue'
export { PatientReminders, type PatientRemindersProps }

View File

@ -0,0 +1,10 @@
.patient-request {
display: flex;
gap: toRem(6);
&__tooltip-content {
@include column(toRem(12));
align-items: flex-start;
}
}

View File

@ -0,0 +1,37 @@
<template>
<div :class="bem()">
<tag-component v-if="request.length" :text="request[0]" />
<tooltip-component
v-if="request.length > 1"
title="Запрос"
icon="bell"
:class="bem('tooltip')"
>
<template v-slot:parent>
<tag-component :text="countText" hoverable view="grey" />
</template>
<template v-slot:content>
<div :class="bem('tooltip-content')">
<tag-component
v-for="text in request"
:key="text"
:text="text"
/>
</div>
</template>
</tooltip-component>
</div>
</template>
<script setup lang="ts" bem-block="patient-request">
import { computed } from 'vue'
export type PatientRequestProps = {
// FIXME change type on Request[]
request: string
}
const props = defineProps<PatientRequestProps>()
const countText = computed(() => `+${props.request.length - 1}`)
</script>

View File

@ -0,0 +1,3 @@
import PatientRequest, { type PatientRequestProps } from './PatientRequest.vue'
export { PatientRequest, type PatientRequestProps }

View File

@ -0,0 +1,39 @@
.patient-survey {
&__card {
&--title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
&--button {
height: 20px;
font-size: toRem(13);
padding: 0;
&:hover {
background-color: white;
}
}
.card {
gap: toRem(20);
}
table {
td,
th {
font-size: toRem(13);
line-height: toRem(20);
padding: toRem(4) 0;
}
}
p {
font-size: toRem(13);
color: var(--dark-64);
}
}
}

View File

@ -0,0 +1,93 @@
<template>
<div :class="bem('card')">
<card-component rounded size="m">
<template #header>
<div class="row justify-between items-center">
<h4 :class="bem('card--title')">Опросники</h4>
<button-component
icon="plus"
view="secondary"
size="xs"
@click="openSelectQuestionnaireModal"
/>
</div>
</template>
<table v-if="survey?.length">
<tr v-for="(s, i) in survey" :key="i">
<th align="left">{{ s.title }}</th>
<td>
<progress-bar
height="5"
:percent="s.percent"
:total="s.total"
:answers="s.answers"
row
></progress-bar>
</td>
</tr>
</table>
<p v-else>Пока нет ни одного опросника</p>
<template v-if="survey?.length" #actions>
<button-component
text="Посмотреть все"
view="secondary"
text-position="left"
:class="bem('card--button')"
@click="moveToQuestionnaireStep"
/>
</template>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="PatientSurvey">
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { useModalsStore, ModalsName } from '@/widgets'
import {
usePatientStore,
ProgressBar,
useMedicalStore,
PatientStep,
} from '@/entities'
export type PatientSurveyCardProps = {}
const { survey, patientStep } = storeToRefs(usePatientStore())
const { surveyList } = storeToRefs(useMedicalStore())
const customerId = useRoute().params.id
const { setModal, closeModal } = useModalsStore()
const { setSurveyList, addSurveyToPatient } = useMedicalStore()
const sendSelectQuestionnaires = async (data: any[]) => {
await addSurveyToPatient({
customer_id: Number(customerId),
survey_ids: data.map(x => Number(x)),
})
closeModal()
}
const openSelectQuestionnaireModal = async () => {
await setSurveyList('')
setModal(ModalsName.SELECTQUESTIONAIRE, {
title: 'Отправить опросник на заполнение',
description: 'Все опросники',
success: {
text: 'Отправить',
cb: sendSelectQuestionnaires,
},
cansel: {
text: 'Отмена',
cb: closeModal,
},
data: surveyList.value,
})
}
const moveToQuestionnaireStep = () => {
patientStep.value = PatientStep.QUESTIONNAIRE
}
</script>

View File

@ -0,0 +1,5 @@
import PatientSurveyCard, {
type PatientSurveyCardProps,
} from './PatientSurveyCard.vue'
export { PatientSurveyCard, type PatientSurveyCardProps }

View File

@ -0,0 +1,27 @@
.progress {
display: flex;
&.row {
flex-direction: row;
align-items: center;
gap: toRem(16);
}
&.column {
flex-direction: column;
align-items: flex-start;
gap: toRem(8);
}
&__bar {
width: 100%;
overflow: hidden;
position: relative;
background: var(--grey-main);
}
.value {
height: inherit;
border-radius: inherit;
background: var(--green-main);
}
}

View File

@ -0,0 +1,50 @@
<template>
<div :class="classes">
<div :class="bem('bar')" v-bind="barStyle">
<div class="value" v-bind="barValueStyle"></div>
</div>
<span>{{ answers }}/{{ total }}</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import useBem from 'vue3-bem'
export type ProgressBarProps = {
borderRadius?: string | number
percent: string | number | null
height?: string
total: number
answers: number
row?: boolean
column?: boolean
}
const props = withDefaults(defineProps<ProgressBarProps>(), {
height: '6',
borderRadius: '25',
row: false,
column: false,
})
/** --------------- State ---------------- */
const bem = useBem('progress')
const classes = computed(() => [
...bem(),
{ row: props.row },
{ column: props.column },
])
const barStyle = computed(() => ({
style: {
height: `${props.height}px`,
'border-radius': `${props.borderRadius}px`,
},
}))
const barValueStyle = computed(() => ({
style: {
width: `${props.percent || 0}%`,
},
}))
</script>

View File

@ -0,0 +1,3 @@
import ProgressBar, { type ProgressBarProps } from './ProgressBar.vue'
export { ProgressBar, type ProgressBarProps }

View File

@ -0,0 +1,27 @@
.questionnaire-card {
&__title {
@include fontSize(
b-16,
(
weight: 500,
)
);
}
&__subtitle {
display: block;
margin-top: toRem(8);
@include fontSize(s-13);
color: var(--dark-64);
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: toRem(20);
}
.card {
gap: toRem(20);
}
}

View File

@ -0,0 +1,42 @@
<template>
<div :class="bem('')">
<card-component rounded size="m">
<template #header>
<header>
<h4 :class="bem('title')">{{ title }}</h4>
<span v-if="!assigned" :class="bem('subtitle')">{{
questions
}}</span>
</header>
</template>
<div v-if="assigned" :class="bem('body')">
<progress-bar
:answers="answers"
:percent="percent"
:total="Number(questions)"
height="12"
column
/>
</div>
<div :class="bem('actions')">
<slot name="actions" />
</div>
</card-component>
</div>
</template>
<script setup lang="ts" bem-block="QuestionnaireCard">
import { ProgressBar } from '@/entities'
export type QuestionnaireCardProps = {
title: string
answers?: number
questions: number | string
percent?: number
assigned?: boolean
}
withDefaults(defineProps<QuestionnaireCardProps>(), {
assigned: false,
answers: 0,
percent: 0,
})
</script>

View File

@ -0,0 +1,5 @@
import QuestionnaireCard, {
type QuestionnaireCardProps,
} from './QuestionnaireCard.vue'
export { QuestionnaireCard, type QuestionnaireCardProps }

View File

@ -0,0 +1,15 @@
export * from './PatientRequest'
export * from './PatientNavigation'
export * from './ProgressBar'
export * from './PatientFilesCard'
export * from './PatientHealthMatrix'
export * from './PatientBasicInfo'
export * from './PatientSurveyCard'
export * from './PatientReminders'
export * from './EditableCard'
export * from './InitialAppointment'
export * from './QuestionnaireCard'
export * from './EmptySurvey'
export * from './InitialHealthMatrix'
export * from './InitialPurpose'
export * from './EditableInput'

View File

@ -0,0 +1,16 @@
import type { AxiosResponse } from 'axios'
import { baseApi } from '@/shared'
import type { User } from '../lib'
export const fetchUser = (): Promise<UserAPI.GET.FetchUser.Response> =>
baseApi.get('user')
export namespace UserAPI {
export namespace GET {
export namespace FetchUser {
export type Response = AxiosResponse<{
data: User
}>
}
}
}

View File

@ -0,0 +1,4 @@
export * from './model'
export * from './lib'
export * from './api'
export * from './ui'

View File

@ -0,0 +1 @@
export * from './types'

View File

@ -0,0 +1,24 @@
export type User = {
id: number
name: string
email: string
birthdate: Maybe<string>
city: Maybe<string>
marital: Maybe<string>
children_count: Maybe<number>
profession: Maybe<string>
contact: Maybe<string>
anamnesis: Maybe<string>
asking: Maybe<string>
roles: UserRole[]
}
export type UserRole = {
id: number
name: UserRoles
guard_name: number
}
export enum UserRoles {
ADMIN = 'admin',
}

View File

@ -0,0 +1 @@
export * from './module'

View File

@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { Stores } from '@/shared'
import { fetchUser } from '../../api'
import type { User } from '../../lib'
export const useUserStore = defineStore(Stores.USER, () => {
const currentUser = ref<Maybe<User>>(null)
const setCurrentUser = async () => {
const { data } = await fetchUser()
currentUser.value = data.data
}
return {
currentUser,
setCurrentUser,
}
})

View File

@ -0,0 +1,17 @@
.user-avatar {
&__img {
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
&.noavatar {
background: var(--purple-8-bg);
cursor: pointer;
}
}
input[type='file'] {
display: none;
}
}

View File

@ -0,0 +1,59 @@
<template>
<label :class="bem()">
<input
type="file"
id="upload-avatar"
accept="image/*"
@change="uploadAvatar"
/>
<img
v-if="avatar"
v-lazy="configs.mainHost + avatar"
alt="avatar"
:class="bem('img')"
@click="setAvatar"
/>
<div
v-else
:class="[
bem('img'),
'noavatar',
'row',
'justify-center',
'items-center',
]"
@click="setAvatar"
>
<icon-base-component name="camera" size="xs" view="brand" />
</div>
</label>
</template>
<script setup lang="ts" bem-block="UserAvatar">
import configs from '@/app/configs'
export type UserAvatarProps = {
avatar: string // url image
userId: number | string
}
const emit = defineEmits<{
(e: 'update:avatar', data: FormData): void
}>()
const props = defineProps<UserAvatarProps>()
/**------------- Methods --------------- */
const setAvatar = () => {
const input = document.getElementById('upload-avatar')
input?.click()
}
const uploadAvatar = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
const formdata = new FormData()
formdata.append('avatar', file)
formdata.append('user_id', String(props.userId))
emit('update:avatar', formdata)
}
</script>

View File

@ -0,0 +1,3 @@
import UserAvatar, { type UserAvatarProps } from './UserAvatar.vue'
export { UserAvatar, type UserAvatarProps }

View File

@ -0,0 +1 @@
export * from './UserAvatar'