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

34
src/app/App.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<transition-fade-component>
<sidebar-component v-if="!isHideSidebar" />
</transition-fade-component>
<router-view v-slot="{ Component }">
<transition name="fade-page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
<modals-component />
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import {
Modals as ModalsComponent,
Sidebar as SidebarComponent,
} from '@/widgets'
import { useUserStore } from '@/entities'
import { Routes } from '@/shared'
const routesWithoutSidebar: Routes[] = [Routes.LOGIN]
const { setCurrentUser } = useUserStore()
const route = useRoute()
const isHideSidebar = computed(() =>
routesWithoutSidebar.includes(route.name as Routes),
)
onMounted(async () => {
await setCurrentUser()
})
</script>

7
src/app/configs.ts Normal file
View File

@ -0,0 +1,7 @@
const mainHost = import.meta.env.MAIN_HOST || 'https://human.dmtay.ru'
export default {
mainHost,
baseURL: mainHost + '/api/',
medicalURL: mainHost + '/medical/',
}

143
src/app/createApp.ts Normal file
View File

@ -0,0 +1,143 @@
import VueDatePicker from '@vuepic/vue-datepicker'
import { createApp, defineAsyncComponent, h } from 'vue'
import Vue3Toasity, { type ToastContainerOptions } from 'vue3-toastify'
import App from '@/app/App.vue'
import { PageLayout } from '@/widgets'
import {
AuthCard,
AuthForm,
Button,
ButtonMenu,
Card,
FileCard,
IconBase,
type IconNames,
InfinityLoading,
Input,
Link,
Logo,
Spinner,
Tabs,
Tag,
ToastIcon,
Tooltip,
TransitionFade,
UserBase,
Dropdown,
Textarea,
} from '@/shared'
export const create = () => {
const app = createApp(App)
app.use(Vue3Toasity, {
autoClose: 2000,
hideProgressBar: true,
pauseOnHover: false,
icon: ({ type }) =>
h(ToastIcon, {
view: type,
}),
closeButton: ({ closeToast }) =>
h(IconBase, {
name: 'close' as IconNames,
class: 'Toastify__close-button',
onClick: closeToast,
}),
} as ToastContainerOptions)
app.component('VueDatePicker', VueDatePicker)
app.component('icon-base-component', IconBase)
app.component('button-component', Button)
app.component('card-component', Card)
app.component('file-card-component', FileCard)
app.component('transition-fade-component', TransitionFade)
app.component('input-component', Input)
app.component('page-layout-component', PageLayout)
app.component('user-base-component', UserBase)
app.component('link-component', Link)
app.component('button-menu-component', ButtonMenu)
app.component('tag-component', Tag)
app.component('tooltip-component', Tooltip)
app.component('spinner-component', Spinner)
app.component('tabs-component', Tabs)
app.component('infinity-loading-component', InfinityLoading)
app.component('logo-component', Logo)
app.component('auth-form-component', AuthForm)
app.component('auth-card-component', AuthCard)
app.component('dropdown-component', Dropdown)
app.component('textarea-component', Textarea)
//modals
app.component(
'modal-dialog-component',
defineAsyncComponent(
() => import('@/widgets/modals/ui/ModalDialog/ModalDialog.vue'),
),
)
app.component(
'modal-edit-patient-component',
defineAsyncComponent(
() => import('@/widgets/modals/ui/EditPatient/EditPatient.vue'),
),
)
app.component(
'modal-select-questionnaire-component',
defineAsyncComponent(
() =>
import(
'@/widgets/modals/ui/SelectQuestionnaires/SelectQuestionnaires.vue'
),
),
)
app.component(
'modal-add-destination-component',
defineAsyncComponent(
() =>
import(
'@/widgets/modals/ui/ModalAddDestination/ModalAddDestination.vue'
),
),
)
app.component(
'modal-add-reminder-component',
defineAsyncComponent(
() =>
import(
'@/widgets/modals/ui/ModalAddReminder/ModalAddReminder.vue'
),
),
)
app.component(
'modal-view-questionnaire-component',
defineAsyncComponent(
() =>
import(
'@/widgets/modals/ui/ModalViewQuestionnaire/ModalViewQuestionnaire.vue'
),
),
)
app.component(
'modal-analysis-hint-component',
defineAsyncComponent(
() => import('@/widgets/modals/ui/AnalysisHints/AnalysisHints.vue'),
),
)
app.component(
'modal-view-optimums-component',
defineAsyncComponent(
() => import('@/widgets/modals/ui/ViewOptimums/ViewOptimums.vue'),
),
)
return app
}

View File

@ -0,0 +1,16 @@
import type { Directive } from 'vue'
export const clickOutside: Directive = {
mounted(element, { value }) {
element.clickOutside = function (event: Event) {
if (!(element === event.target || element.contains(event.target))) {
value(event)
}
}
document.body.addEventListener('click', element.clickOutside)
},
unmounted(element) {
document.body.removeEventListener('click', element.clickOutside)
},
}

View File

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

16
src/app/main.ts Normal file
View File

@ -0,0 +1,16 @@
import '@/app/styles/index.scss'
import { createPinia } from 'pinia'
import VueLazyLoad from 'vue3-lazyload'
import { create } from '@/app/createApp'
import { router } from '@/app/providers/router'
import { clickOutside } from './directives'
export const app = create()
app.use(createPinia())
app.use(router)
app.directive('click-outside', clickOutside)
app.use(VueLazyLoad, {
delay: 300,
})
app.mount('#app')

View File

@ -0,0 +1,44 @@
import { createRouter, createWebHistory } from 'vue-router'
import { type Middleware, type MiddlewareContext } from './middlewares'
import { routes } from './routes'
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
router.beforeEach(async (to, from, next) => {
if (!to.meta.middleware) {
return next()
}
const middleware = to.meta.middleware as Middleware[]
const context: MiddlewareContext = {
to,
from,
next,
}
return middleware[0]({
...context,
next: middlewarePipeline(context, middleware, 1),
})
})
const middlewarePipeline = (
context: MiddlewareContext,
middleware: Middleware[],
index: number,
) => {
const nextMiddleware = middleware[index]
if (!nextMiddleware) {
return context.next
}
return () => {
const nextPipeline = middlewarePipeline(context, middleware, index + 1)
nextMiddleware({ ...context, next: nextPipeline })
}
}

View File

@ -0,0 +1,43 @@
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { tokenValidate } from '@/features'
import { Routes } from '@/shared'
export type MiddlewareContext = {
to: RouteLocationNormalized
from: RouteLocationNormalized
next: NavigationGuardNext
}
export type Middleware = (context: MiddlewareContext) => any
export const guest = async ({ next }: MiddlewareContext) => {
const isAuth = await tokenValidate()
if (isAuth) {
return next({
name: Routes.INDEX,
})
}
return next()
}
export const auth = async ({ next }: MiddlewareContext) => {
const isAuth = await tokenValidate()
if (!isAuth) {
return next({
name: Routes.LOGIN,
})
}
return next()
}
export const patient = ({ to, next }: MiddlewareContext) => {
if (to.params.id) {
return next()
}
return next({ name: Routes.MY_PATIENTS })
}

View File

@ -0,0 +1,124 @@
import type { RouteRecordRaw } from 'vue-router'
import { Routes } from '@/shared'
import { auth, guest, patient } from './middlewares'
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: Routes.INDEX,
redirect: { name: Routes.MY_PATIENTS },
meta: {
middleware: [auth],
},
},
{
path: '/patient/:id?',
name: Routes.PATIENT,
props: true,
component: () => import('@/pages/patient/ui/Patient/Patient.vue'),
meta: {
middleware: [auth, patient],
page: {
title: 'Пациента',
},
},
},
{
path: '/my-patients',
name: Routes.MY_PATIENTS,
component: () =>
import('@/pages/patients/ui/MyPatients/MyPatients.vue'),
meta: {
middleware: [auth],
page: {
title: 'Мои пациенты',
},
},
},
{
path: '/requests-patients',
name: Routes.REQUESTS_PATIENTS,
component: () =>
import('@/pages/patients/ui/RequestsPatients/RequestsPatients.vue'),
meta: {
middleware: [auth],
page: {
title: 'Запросы на ведение',
},
},
},
{
path: '/calendar',
name: Routes.CALENDAR,
component: () => import('@/pages/calendar/ui/Calendar/Calendar.vue'),
meta: {
middleware: [auth],
page: {
title: 'Календарь',
},
},
},
{
path: '/chat',
name: Routes.CHAT,
component: () => import('@/pages/chat/ui/Chat/Chat.vue'),
meta: {
middleware: [auth],
page: {
title: 'Чат',
},
},
},
{
path: '/videochat',
name: Routes.VIDEOCHAT,
component: () => import('@/pages/videochat/ui/Videochat/Videochat.vue'),
meta: {
middleware: [auth],
page: {
title: 'Видеочат',
},
},
},
{
path: '/library',
name: Routes.LIBRARY,
component: () => import('@/pages/library/ui/Library/Library.vue'),
meta: {
middleware: [auth],
page: {
title: 'Моя библиотека',
},
},
},
{
path: '/login',
name: Routes.LOGIN,
component: () => import('@/pages/login/ui/LoginPage/LoginPage.vue'),
meta: {
middleware: [guest],
},
},
{
path: '/support',
name: Routes.SUPPORT,
component: () => import('@/pages/support/ui/Support/Support.vue'),
meta: {
middleware: [auth],
page: {
title: 'Поддержка',
},
},
},
{
path: '/profile',
name: Routes.PROFILE,
component: () => import('@/pages/profile/ui/Profile/Profile.vue'),
meta: {
middleware: [auth],
page: {
title: 'Профиль',
},
},
},
]

16
src/app/styles/index.scss Normal file
View File

@ -0,0 +1,16 @@
@import './utils/utils';
@import './utils/reset';
@import './utils/common';
@import './utils/fonts';
@import './utils/icons';
@import './utils/toast';
@import './utils/datetimepocker';
@import '../../shared/shared';
@import '../../entities/entities';
@import '../../features/features';
@import '../../widgets/widgets';
@import '../../pages/pages';
@import './utils/animations';

View File

@ -0,0 +1,46 @@
// fade
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
// fade-page
.fade-page-enter-active,
.fade-page-leave-active {
transition: opacity 0.5s linear !important;
}
.fade-page-enter-from,
.fade-page-leave-to {
opacity: 0;
}
// list
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
@keyframes rotate360 {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,121 @@
body {
font-size: toRem(14);
color: var(--dark-main);
font-family: $mainFontFamily, sans-serif;
font-weight: 400;
background-color: var(--white);
overflow-y: hidden;
position: relative;
}
#app {
height: 100vh;
}
img {
height: auto;
width: auto;
transition: opacity 0.3s ease;
&[lazy='loading'] {
opacity: 0;
will-change: transform, opacity;
}
&[lazy='error'] {
}
&[lazy='loaded'] {
opacity: 1;
}
}
a {
color: var(--bright-blue);
text-decoration: none;
}
html {
font-size: 12px;
}
input {
border: none;
}
.scroller {
&::-webkit-scrollbar {
width: 15px;
}
&::-webkit-scrollbar-thumb {
background: var(--dark-64);
background-clip: content-box;
border: 5px solid transparent;
border-radius: 1000px;
}
}
.visually-hidden {
position: absolute;
overflow: hidden;
white-space: nowrap;
margin: 0;
padding: 0;
height: 1px;
width: 1px;
clip: rect(0 0 0 0);
clip-path: inset(100%);
}
.justify-center {
justify-content: center;
}
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.row {
@include row(toRem(4));
}
.column {
@include column(toRem(4));
}
.wrap {
flex-wrap: wrap;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}

View File

@ -0,0 +1,76 @@
@import '@vuepic/vue-datepicker/dist/main.css';
.dp {
$b: &;
font-family: $mainFontFamily !important;
&__theme_light {
--dp-font-size: toRem(15);
--dp-text-color: var(--dark-main);
// --dp-background-color: var(--grey-main);
--dp-input-padding: 12px 30px 12px 12px;
--dp-icon-color: var(--brand-main);
--dp-hover-icon-color: var(--brand-main);
--dp-hover-text-color: var(--dark-main);
--dp-primary-color: var(--brand-main);
}
&__main {
height: inherit;
display: flex;
align-items: center;
> div {
height: inherit;
width: inherit;
}
}
&__input {
border: none;
background-color: transparent;
&_wrap {
height: inherit;
display: flex;
align-items: center;
}
}
&__month_year_wrap {
font-size: toRem(17);
#{$b}__btn:last-child {
color: var(--brand-main);
}
}
&__today {
border: none;
position: relative;
&::after {
content: '';
position: absolute;
width: 50%;
height: 2px;
bottom: 5px;
background: #000;
}
}
&__calendar {
font-size: toRem(15);
&_header {
font-weight: 400;
text-transform: uppercase;
}
}
&__container_block {
font-size: toRem(15);
}
&__overlay_container {
font-size: toRem(15);
}
}

View File

@ -0,0 +1,6 @@
@include newFont($mainFontFamily, 'NeueHaasUnica-Regular', 400);
@include newFont($mainFontFamily, 'NeueHaasUnica-Medium', 500);
@include newFont($mainFontFamily, 'NeueHaasUnica-Bold', 700);
@include newFont($mainFontFamily, 'NeueHaasUnica-Black', 900);
@include newFont($decorationFontFamily, 'TinkoffSans-Bold', 700);

View File

@ -0,0 +1,40 @@
@use 'sass:math';
@function toRem($size) {
$remSize: math.div($size, 12) * 1rem;
@return $remSize;
}
@mixin fontSize($name, $options: ()) {
@each $tagName, $tagValue in $tags {
@if $tagName == $name {
$selectedTag: map-get($tags, $tagName);
$fontSize: nth($selectedTag, 1);
$lineHeight: nth($selectedTag, 2);
font-size: $fontSize;
line-height: $lineHeight;
@if map-get($options, weight) {
font-weight: map-get($options, weight);
}
@if map-get($options, style) {
font-style: map-get($options, style);
}
@if map-get($options, uppercase) {
text-transform: uppercase;
}
@if map-get($options, letter-spacing) {
letter-spacing: map-get($options, letter-spacing);
}
@if map-get($options, line-height) {
line-height: map-get($options, line-height);
}
}
}
}

View File

@ -0,0 +1,128 @@
@font-face {
font-family: "icons";
src: url("/fonts/icons/icons.woff?98e4dbf90127ac6ec671d6317de9533e") format("woff"),
url("/fonts/icons/icons.woff2?98e4dbf90127ac6ec671d6317de9533e") format("woff2");
}
i[class^="icon-"]:before, i[class*=" icon-"]:before {
font-family: icons !important;
font-style: normal;
font-weight: normal !important;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-arrow-down:before {
content: "\f101";
}
.icon-arrow-left:before {
content: "\f102";
}
.icon-arrow-narrow-down:before {
content: "\f103";
}
.icon-arrow-narrow-up-right:before {
content: "\f104";
}
.icon-arrow-narrow-up:before {
content: "\f105";
}
.icon-arrow-right:before {
content: "\f106";
}
.icon-bell:before {
content: "\f107";
}
.icon-book-open:before {
content: "\f108";
}
.icon-calculator:before {
content: "\f109";
}
.icon-calendar:before {
content: "\f10a";
}
.icon-camera:before {
content: "\f10b";
}
.icon-check-circle:before {
content: "\f10c";
}
.icon-check-heart:before {
content: "\f10d";
}
.icon-check:before {
content: "\f10e";
}
.icon-clock:before {
content: "\f10f";
}
.icon-close:before {
content: "\f110";
}
.icon-date:before {
content: "\f111";
}
.icon-dots-vertical:before {
content: "\f112";
}
.icon-file:before {
content: "\f113";
}
.icon-help-circle:before {
content: "\f114";
}
.icon-info-circle:before {
content: "\f115";
}
.icon-info:before {
content: "\f116";
}
.icon-link:before {
content: "\f117";
}
.icon-message-text:before {
content: "\f118";
}
.icon-pencil-line:before {
content: "\f119";
}
.icon-placeholder:before {
content: "\f11a";
}
.icon-plus:before {
content: "\f11b";
}
.icon-search:before {
content: "\f11c";
}
.icon-switch-vertical:before {
content: "\f11d";
}
.icon-trash:before {
content: "\f11e";
}
.icon-tui-marker:before {
content: "\f11f";
}
.icon-user-edit:before {
content: "\f120";
}
.icon-user-plus:before {
content: "\f121";
}
.icon-user:before {
content: "\f122";
}
.icon-users-right:before {
content: "\f123";
}
.icon-video-recorder:before {
content: "\f124";
}
.icon-x-circle:before {
content: "\f125";
}

View File

@ -0,0 +1,111 @@
@mixin newFont($family, $pathName, $weight) {
@font-face {
font-family: '#{$family}';
src:
url('/fonts/#{$pathName}.woff2') format('woff2'),
url('/fonts/#{$pathName}.woff') format('woff');
font-weight: #{$weight};
font-display: swap;
}
}
@mixin ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
@mixin center($position: 'both') {
position: absolute;
@if $position == 'vertical' {
top: 50%;
transform: translateY(-50%);
} @else if $position == 'horizontal' {
left: 50%;
transform: translateX(-50%);
} @else if $position == 'both' {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} @else if $position == 'stretch' {
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
@mixin column($gap: 0px) {
display: flex;
flex-direction: column;
gap: $gap;
}
@mixin row($gap: 0px) {
display: flex;
flex-direction: row;
gap: $gap;
}
@mixin responsive($breakpoint) {
@if $breakpoint == 'sm' {
@media only screen and (max-width: 576px) {
@content;
}
}
@if $breakpoint == 'md' {
@media only screen and (max-width: 768px) {
@content;
}
}
@if $breakpoint == 'lg' {
@media only screen and (max-width: 992px) {
@content;
}
}
@if $breakpoint == 'xl' {
@media only screen and (max-width: 1200px) {
@content;
}
}
@if $breakpoint == 'xxl' {
@media only screen and (max-width: 1400px) {
@content;
}
}
}
@mixin modalBaseStyles() {
&__content {
@include center();
@include column();
width: toRem(490);
background-color: var(--white);
border-radius: $borderRadius24;
padding: toRem(32);
}
&__title {
@include fontSize(
h2,
(
weight: 500,
)
);
}
&__description {
margin-top: toRem(20);
@include fontSize(s-13);
}
&__buttons {
display: flex;
justify-content: flex-end;
gap: toRem(12);
margin-top: toRem(24);
}
}

View File

@ -0,0 +1,571 @@
//** * Modern CSS Reset Tweaks * ================================================== */
html {
-webkit-text-size-adjust: 100%;
&:focus-within {
scroll-behavior: smooth;
}
}
body {
text-size-adjust: 100%;
position: relative;
width: 100%;
min-height: 100vh;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeSpeed;
}
/* Box sizing normalization */
*,
::after,
::before {
box-sizing: border-box;
}
/* Elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/**
* CSS Reset Tweaks
*
* http://meyerweb.com/eric/tools/css/reset/
* v2.0-modified | 20110126
* License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
font-size: 100%;
font: inherit;
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
}
/* make sure to set some focus styles for accessibility */
:focus {
outline: 0;
}
/* HTML5 display-role reset for older browsers */
main,
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
&:before,
&:after {
content: '';
content: none;
}
}
/**
* Input Reset
*/
input:required,
input {
box-shadow: none;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset;
}
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type='search'] {
-webkit-appearance: none;
-moz-appearance: none;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
input {
&:focus {
outline: none;
}
}
/**
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
max-width: 100%;
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
*/
[hidden] {
display: none;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: none;
}
/* Make images easier to work with */
img {
max-width: 100%;
display: inline-block;
vertical-align: middle;
height: auto;
}
/* Make pictures easier to work with */
picture {
display: inline-block;
}
/**
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
* Correct `select` style inheritance in Firefox 4+ and Opera.
*/
button,
select {
text-transform: none;
}
button,
html input[type='button'],
input[type='reset'],
input[type='submit'] {
-webkit-appearance: button;
cursor: pointer;
border: 0;
background: transparent;
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
[disabled] {
pointer-events: none;
}
/**
* 1. Address box sizing set to content-box in IE 8/9.
*/
input[type='checkbox'],
input[type='radio'] {
padding: 0;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type='search'] {
-webkit-appearance: textfield;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Remove inner padding and border in Firefox 3+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
button {
border: 0;
background: transparent;
}
textarea {
overflow: auto;
vertical-align: top;
resize: vertical;
}
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
text-indent: 0;
}
/**
* Based on normalize.css v8.0.1
* github.com/necolas/normalize.css
*/
hr {
box-sizing: content-box;
overflow: visible;
background: #000;
border: 0;
height: 1px;
line-height: 0;
margin: 0;
padding: 0;
page-break-after: always;
width: 100%;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
*/
pre {
font-family: monospace, monospace;
font-size: 100%;
}
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
text-decoration: none;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 75%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -5px;
}
sup {
top: -5px;
}
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1;
margin: 0;
padding: 0;
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
outline: 0;
}
legend {
color: inherit;
white-space: normal;
display: block;
border: 0;
max-width: 100%;
width: 100%;
}
fieldset {
min-width: 0;
}
body:not(:-moz-handler-blocked) fieldset {
display: block;
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/*
* Misc
* ========================================================================== */
template {
display: none;
}

View File

@ -0,0 +1,43 @@
@import 'vue3-toastify/dist/index.css';
:root {
--toastify-text-color-light: var(--dark-main);
--toastify-toast-min-height: toRem(48);
}
.Toastify {
font-family: $mainFontFamily, sans-serif;
&__toast {
border-radius: $borderRadius8;
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.12);
padding: toRem(12) toRem(16);
&-body {
margin: 0;
padding: 0;
}
&-content {
@include fontSize(s-13);
}
&-icon {
font-size: toRem(16);
}
&--success {
background-color: var(--green-bg);
}
&--error {
background-color: var(--critical-bg);
}
}
&__close-button {
color: var(--dark-main);
font-size: toRem(24);
height: 100%;
}
}

View File

@ -0,0 +1,8 @@
@import 'functions';
@import 'vars/colors';
@import 'vars/other';
@import 'vars/spaces';
@import 'vars/typography';
@import 'mixins';

View File

@ -0,0 +1,71 @@
:root {
--white: #fff;
// main
--dark-main: #364153;
--berry-main: #ef6093;
--grey-main: #f2f5f9;
--brand-main: #a241f0;
--berry-main: #ef6093;
--orange-main: #fab619;
--purple-main: #775abf;
--green-main: #b2d677;
--critical-main: #ed6a5eff;
//border
--grey-border: #dfe4ed;
--green-border: rgba(135, 163, 88, 1);
//background
--green-bg: rgba(236, 244, 225, 1);
--critical-bg: #f6c2bd;
--brand-4-bg: rgba(162, 65, 240, 0.04); //#F6EEFF
--blue-20-bg: rgba(134, 200, 241, 0.2); //#f4f5fd
--purple-bg: #f6eeff;
--purple-8-bg: rgba(119, 90, 191, 0.08);
//hover
--brand-hover: #9138d8;
--grey-hover: #eaeffa;
//states
--berry-8: rgba(239, 96, 147, 0.08);
--dark-4: rgba(54, 65, 83, 0.04);
--dark-14: rgba(54, 65, 83, 0.14);
--dark-20: rgba(54, 65, 83, 0.2);
--berry-14: rgba(239, 96, 147, 0.14);
--berry-20: rgba(239, 96, 147, 0.2);
--dark-32: rgba(54, 65, 83, 0.32);
--green-32: rgba(206, 248, 137, 0.32);
--green-64: rgba(178, 214, 119, 0.64);
--dark-64: #364153a3;
--grey-64: #f8fafc;
--blue-20: #86c8f133;
--brand-20: rgba(162, 65, 240, 0.2);
--brand-64: rgba(162, 65, 240, 0.64);
--brand-8: rgba(162, 65, 240, 0.08);
--brand-4: rgba(162, 65, 240, 0.04);
--dark-blue: #3388bc;
--blue-14: rgba(134, 200, 241, 0.14);
--green-14: rgba(178, 214, 119, 0.14);
--green-20: rgba(178, 214, 119, 0.2);
--orange-20: rgba(255, 219, 117, 0.2);
--orange-32: rgba(255, 219, 117, 0.32);
--berry-4: rgba(239, 96, 147, 0.04);
--berry-8: rgba(239, 96, 147, 0.08);
--berry-14: rgba(239, 96, 147, 0.14);
--text-primary: rgba(0, 0, 0, 0.8);
--day-Base-base-04: #dddfe0;
// gradients
--brand-linear: linear-gradient(138deg, #a241f0 2.35%, #775abf 96.98%);
--ch-linear1: linear-gradient(
112deg,
#ac52f4 0.73%,
#a9139a 33.95%,
#5a409c 104.31%
);
--dp-font-family: $mainFontFamily;
}

View File

@ -0,0 +1,11 @@
$borderRadius2: toRem(2);
$borderRadius4: toRem(4);
$borderRadius6: toRem(6);
$borderRadius8: toRem(8);
$borderRadius10: toRem(10);
$borderRadius12: toRem(12);
$borderRadius16: toRem(16);
$borderRadius20: toRem(20);
$borderRadius24: toRem(24);
$widthSideBar: toRem(260);

View File

View File

@ -0,0 +1,38 @@
$mainFontFamily: 'NeueHaasUnicaW1G';
$decorationFontFamily: 'Tinkoff Sans';
$icon: 'icon', sans-serif;
$tags: (
h2: (
toRem(28),
normal,
),
h3: (
toRem(20),
normal,
),
b-16: (
toRem(16),
normal,
),
b-15: (
toRem(15),
normal,
),
b-14: (
toRem(14),
normal,
),
s-13: (
toRem(13),
normal,
),
s-12: (
toRem(12),
toRem(16.5),
),
s-11: (
toRem(11),
toRem(16),
),
);

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;
}
}

Some files were not shown because too many files have changed in this diff Show More