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

20
.eslintrc.js Normal file
View File

@ -0,0 +1,20 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
'@feature-sliced',
],
rules: {
'vue/multi-word-component-names': 0,
'import/order': 'warn',
},
parserOptions: {
ecmaVersion: 'latest',
},
}

41
.fantasticonrc.js Normal file
View File

@ -0,0 +1,41 @@
module.exports = {
inputDir: './src/shared/assets/icons/formatting', // (required)
outputDir: './public/fonts/icons', // (required)
fontTypes: ['woff', 'woff2'],
assetTypes: ['ts', 'css'],
fontsUrl: '/fonts/icons',
normalize: true,
formatOptions: {
json: {
// render the JSON human readable with two spaces indentation (default is none, so minified)
indent: 2,
},
ts: {
// select what kind of types you want to generate (default `['enum', 'constant', 'literalId', 'literalKey']`)
types: ['literalId'],
// render the types with `'` instead of `"` (default is `"`)
singleQuotes: true,
// customise names used for the generated types and constants
// enumName: 'MyIconType',
// constantName: 'IconsCodes',
literalIdName: 'IconNames',
// literalKeyName: 'IconKey',
},
},
// Use a custom Handlebars template
templates: {
// css: './my-custom-tp.css.hbs',
},
pathOptions: {
ts: './src/shared/lib/types/icons.ts',
// json: './misc/icon-codepoints.json',
css: './src/app/styles/utils/icons.scss',
},
// getIconId: ({
// basename, // `string` - Example: 'foo';
// relativeDirPath, // `string` - Example: 'sub/dir/foo.svg'
// absoluteFilePath, // `string` - Example: '/var/icons/sub/dir/foo.svg'
// relativeFilePath, // `string` - Example: 'foo.svg'
// index, // `number` - Example: `0`
// }) => [index, basename].join('_'), // '0_foo'
}

52
.gitignore vendored Normal file
View File

@ -0,0 +1,52 @@
# These are some examples of commonly ignored file patterns.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
node_modules/
dist/
# Compiled Java class files
*.class
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store
# Generated by Windows
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv
# CSSComb config
csscomb.json

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"tabWidth": 4,
"bracketSpacing": true,
"singleQuote": true,
"trailingComma": "all",
"semi": false,
"jsxSingleQuote": true,
"arrowParens": "avoid",
"printWidth": 80
}

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# vue-health
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/app/main.ts"></script>
</body>
</html>

8278
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "vue-health",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"format-icons": "node src/shared/utils/npm/formattingIcons.js",
"icons": "fantasticon"
},
"dependencies": {
"@vuepic/vue-datepicker": "^7.4.0",
"axios": "^1.6.1",
"fantasticon": "^2.0.0",
"oslllo-svg-fixer": "^3.0.0",
"pinia": "^2.1.7",
"v3-infinite-loading": "^1.3.1",
"vite-plugin-vue3-bem": "^1.0.12",
"vue": "^3.3.4",
"vue-router": "^4.2.5",
"vue3-bem": "^1.0.8",
"vue3-lazyload": "^0.3.8",
"vue3-toastify": "^0.1.14"
},
"devDependencies": {
"@feature-sliced/eslint-config": "^0.1.0-beta.6",
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/babel-plugin-jsx": "^1.1.5",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"node-sass": "^9.0.0",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"typescript": "~5.2.0",
"vite": "^4.4.11",
"vue-tsc": "^1.8.19"
},
"overrides": {
"fantasticon": {
"glob": "7.2.0"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,56 @@
@font-face {
font-family: "icons";
src: url("./public/fonts/icons/icons.woff?ce6361810f7aeed17b09d8e9e447cd48") format("woff"),
url("./public/fonts/icons/icons.woff2?ce6361810f7aeed17b09d8e9e447cd48") 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-book-open:before {
content: "\f101";
}
.icon-calendar:before {
content: "\f102";
}
.icon-dots-vertical:before {
content: "\f103";
}
.icon-help-circle:before {
content: "\f104";
}
.icon-message-text:before {
content: "\f105";
}
.icon-plus:before {
content: "\f106";
}
.icon-search:before {
content: "\f107";
}
.icon-trash:before {
content: "\f108";
}
.icon-user-edit:before {
content: "\f109";
}
.icon-user-plus:before {
content: "\f10a";
}
.icon-user:before {
content: "\f10b";
}
.icon-users-right:before {
content: "\f10c";
}
.icon-video-recorder:before {
content: "\f10d";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,36 @@
<svg width="169" height="140" viewBox="0 0 169 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M121.722 24.4059L120.267 27.2202L119.052 26.592L119.934 24.8864L114.614 22.1368L113.733 23.8424L112.517 23.2142L113.972 20.4L114.814 20.8353C115.681 20.2297 116.746 18.9022 118.975 14.7727L124.155 17.4507L120.805 23.932L121.722 24.4059ZM122.303 17.8981L119.627 16.515C118.076 19.3334 116.994 20.7733 116.168 21.5351L119.526 23.2708L122.303 17.8981Z" fill="#364153"/>
<path d="M131.765 26.2656L125.916 29.697C124.49 30.5299 123.673 30.4664 122.578 29.5694C122.373 29.4022 122.211 29.2223 122.102 29.0712L122.809 28.2076C122.899 28.3436 123.034 28.5006 123.201 28.6375C123.777 29.1088 124.202 29.1004 124.957 28.6329L127.122 22.4649L128.255 23.3923L126.561 27.8675L126.579 27.8827L130.669 25.3686L131.765 26.2656Z" fill="#364153"/>
<path d="M134.544 33.6684L133.559 37.3708L132.513 36.1492L133.322 33.6247L133.306 33.6064L130.687 34.0161L129.641 32.7946L133.451 32.3922L134.373 28.9487L135.411 30.1612L134.719 32.3968L134.734 32.4151L137.05 32.0755L138.088 33.2879L134.544 33.6684Z" fill="#364153"/>
<path d="M142.127 39.3008C143.05 40.9016 142.545 42.5231 140.83 43.5124C139.115 44.5017 137.458 44.1273 136.535 42.5265C135.611 40.9257 136.111 39.2939 137.826 38.3046C139.541 37.3153 141.204 37.7 142.127 39.3008ZM141.191 39.8404C140.706 38.9985 139.689 38.8924 138.545 39.5519C137.402 40.2115 136.984 41.1449 137.47 41.9869C137.956 42.8289 138.973 42.9349 140.116 42.2754C141.26 41.6159 141.677 40.6824 141.191 39.8404Z" fill="#364153"/>
<path d="M145.332 46.4665L146.173 48.9184C146.686 50.4168 146.395 51.3414 145.407 51.6801C144.715 51.9175 144.204 51.6866 143.793 51.0791L143.77 51.0869C143.879 51.8867 143.558 52.4663 142.832 52.7154C141.81 53.0657 140.943 52.4621 140.465 51.0659L139.565 48.4438L145.332 46.4665ZM144.879 48.0301L143.335 48.5595L143.685 49.5811C143.942 50.3303 144.32 50.6192 144.842 50.4402C145.364 50.2611 145.486 49.8009 145.229 49.0517L144.879 48.0301ZM142.506 48.8436L140.883 49.4002L141.241 50.4445C141.521 51.2618 141.915 51.5582 142.471 51.3675C143.016 51.1807 143.144 50.7052 142.864 49.8879L142.506 48.8436Z" fill="#364153"/>
<path d="M142.801 62.7111L142.625 61.3546L145.159 61.0247L144.808 58.3234L142.273 58.6534L142.096 57.2968L148.141 56.51L148.318 57.8665L145.843 58.1887L146.194 60.8899L148.67 60.5677L148.846 61.9243L142.801 62.7111Z" fill="#364153"/>
<path d="M148.914 70.3296C148.782 72.1728 147.455 73.2328 145.48 73.0914C143.505 72.9501 142.343 71.7119 142.475 69.8686C142.607 68.0253 143.935 66.9534 145.91 67.0948C147.885 67.2361 149.046 68.4863 148.914 70.3296ZM147.837 70.2524C147.906 69.2829 147.123 68.6253 145.807 68.5311C144.49 68.4368 143.621 68.9762 143.552 69.9457C143.483 70.9152 144.266 71.5728 145.582 71.6671C146.899 71.7613 147.768 71.222 147.837 70.2524Z" fill="#364153"/>
<path d="M141.818 79.1258C141.614 79.8787 141.888 80.475 142.424 80.7444L142.057 82.0996C141.063 81.5943 140.428 80.4031 140.845 78.8625C141.331 77.0671 142.902 76.361 144.802 76.8751C146.539 77.3454 147.566 78.7421 147.083 80.5259C146.719 81.8696 145.68 82.6325 144.364 82.6246C143.962 82.6401 143.51 82.5673 142.942 82.4137L144.111 78.0931C142.767 77.7789 142.041 78.3034 141.818 79.1258ZM146.11 80.2626C146.326 79.4634 145.962 78.7431 144.974 78.3514L144.168 81.3283C145.224 81.5147 145.897 81.0503 146.11 80.2626Z" fill="#364153"/>
<path d="M26.5794 71.4707L26.8491 72.8363L22.6345 73.6688C21.575 73.8781 20.5132 74.0756 19.4467 74.2496L19.4513 74.2731C20.5736 74.3817 21.6888 74.5161 22.8063 74.6623L27.3305 75.2732L27.649 76.8861L23.6969 79.1712C22.7189 79.7314 21.7386 80.2797 20.7419 80.8069L20.7465 80.8304C21.7991 80.5858 22.8563 80.3648 23.9158 80.1555L28.1304 79.323L28.4001 80.6886L20.0181 82.3443L19.5693 80.0722L23.2928 77.93C24.259 77.3722 25.2652 76.831 26.2571 76.3415L26.2525 76.3179C25.1466 76.2306 24.0102 76.1126 22.9044 75.964L18.6461 75.3985L18.1973 73.1264L26.5794 71.4707Z" fill="#364153"/>
<path d="M26.0272 67.7665C26.0164 66.9866 25.5895 66.4884 24.9999 66.3765L24.9806 64.9727C26.0757 65.1856 27.0131 66.1568 27.0351 67.7526C27.0607 69.6125 25.7438 70.7227 23.776 70.7498C21.9762 70.7746 20.6054 69.7133 20.58 67.8655C20.5609 66.4736 21.3509 65.4547 22.6182 65.1012C23.0005 64.9759 23.4558 64.9216 24.0437 64.9135L24.1053 69.3891C25.4845 69.3221 26.0389 68.6184 26.0272 67.7665ZM21.5879 67.8516C21.5993 68.6795 22.1475 69.2721 23.2051 69.3775L23.1626 66.2938C22.0961 66.4045 21.5767 67.0357 21.5879 67.8516Z" fill="#364153"/>
<path d="M27.876 57.4975L27.6818 58.8517L25.1517 58.4887L24.7648 61.1851L27.2949 61.5481L27.1007 62.9022L21.0665 62.0365L21.2607 60.6824L23.7314 61.0368L24.1183 58.3405L21.6475 57.986L21.8418 56.6319L27.876 57.4975Z" fill="#364153"/>
<path d="M25.1252 49.0631L24.5474 50.982L29.3849 52.4386L28.987 53.76L24.1495 52.3034L23.5682 54.2337L22.5686 53.9327L24.1256 48.7621L25.1252 49.0631Z" fill="#364153"/>
<path d="M26.1437 43.8016C26.8231 42.3442 27.9213 41.8632 29.3678 42.5375L31.4343 43.5009C32.0868 43.8051 32.569 43.9637 32.9576 44.0389L32.4353 45.1592C32.1707 45.102 31.8887 44.997 31.6276 44.8753L31.6175 44.8971C32.0172 45.5733 32.0327 46.3352 31.6423 47.1727C31.1048 48.3255 30.0741 48.7189 29.1279 48.2777C28.2252 47.8569 27.8863 47.0502 28.3764 45.3456C28.5082 44.8642 28.7471 44.1812 28.9079 43.7795L28.6904 43.6781C27.8747 43.2979 27.3405 43.5917 27.0464 44.2225C26.727 44.9077 26.9353 45.4549 27.5074 45.7614L26.9395 46.9795C25.9207 46.4384 25.4491 45.2917 26.1437 43.8016ZM29.9847 44.2815L29.7128 44.1547C29.5621 44.5347 29.3681 45.093 29.2609 45.4932C29.0213 46.348 29.1237 46.7532 29.624 46.9864C30.1134 47.2146 30.6019 47.0187 30.896 46.3878C31.0735 46.0072 31.1262 45.5816 30.9927 45.2148C30.8251 44.7792 30.572 44.5553 29.9847 44.2815Z" fill="#364153"/>
<path d="M37.0765 37.7533L36.3197 38.8929L32.1112 36.0979L31.142 37.5574C34.6876 40.0273 34.9755 41.0253 34.2054 42.1849C34.0926 42.3548 33.9797 42.4815 33.8634 42.5915L33.0337 42.0405C33.0769 41.9971 33.1333 41.9338 33.1798 41.8638C33.618 41.2041 33.1808 40.5392 29.562 38.0494L31.9984 34.3808L37.0765 37.7533Z" fill="#364153"/>
<path d="M36.6523 32.2231L37.6032 31.1739C38.6507 30.0179 39.7483 29.879 40.6909 30.7331C41.6334 31.5873 41.6029 32.6932 40.5553 33.8492L38.6859 35.9121L34.1687 31.8186L35.0874 30.8049L36.6523 32.2231ZM37.4082 32.9081L38.8487 34.2135L39.7028 33.2709C40.2185 32.7018 40.2533 32.1989 39.7642 31.7557C39.284 31.3206 38.778 31.3964 38.2623 31.9655L37.4082 32.9081Z" fill="#364153"/>
<path d="M48.1371 27.2143L47.0719 28.0726L45.4681 26.0825L43.3471 27.7917L44.9509 29.7819L43.8857 30.6403L40.0606 25.8937L41.1258 25.0353L42.692 26.9788L44.813 25.2696L43.2468 23.3261L44.312 22.4677L48.1371 27.2143Z" fill="#364153"/>
<path d="M49.5247 19.1376C51.1266 18.2161 52.7474 18.7234 53.7348 20.4396C54.7222 22.1559 54.346 23.8121 52.7441 24.7336C51.1423 25.6552 49.511 25.1539 48.5236 23.4376C47.5363 21.7214 47.9229 20.0592 49.5247 19.1376ZM50.0633 20.0738C49.2208 20.5585 49.1136 21.5754 49.7718 22.7195C50.4301 23.8637 51.363 24.2822 52.2056 23.7975C53.0481 23.3128 53.1553 22.2959 52.497 21.1517C51.8388 20.0076 50.9058 19.5891 50.0633 20.0738Z" fill="#364153"/>
<path d="M59.0541 20.7545C59.7808 20.471 60.0976 19.8965 59.9956 19.3051L61.3036 18.7947C61.4881 19.8948 60.9074 21.1133 59.4206 21.6935C57.6878 22.3697 56.1863 21.5258 55.4709 19.6925C54.8165 18.0156 55.3297 16.3598 57.0512 15.688C58.348 15.1819 59.5793 15.5645 60.3547 16.6273C60.6061 16.9414 60.8166 17.3488 61.0304 17.8965L56.8606 19.5237C57.407 20.7919 58.2604 21.0642 59.0541 20.7545ZM57.4177 16.627C56.6463 16.928 56.2837 17.6492 56.5558 18.6766L59.4288 17.5554C58.9511 16.5954 58.1778 16.3304 57.4177 16.627Z" fill="#364153"/>
<path d="M127.718 113.75C128.27 113.2 128.331 112.546 128.001 112.045L128.996 111.054C129.605 111.989 129.56 113.338 128.429 114.464C127.112 115.777 125.398 115.604 124.009 114.21C122.738 112.935 122.547 111.212 123.856 109.908C124.842 108.925 126.123 108.783 127.259 109.447C127.615 109.635 127.971 109.924 128.386 110.34L125.215 113.499C126.223 114.443 127.114 114.351 127.718 113.75ZM124.567 110.622C123.981 111.206 123.937 112.012 124.597 112.845L126.782 110.668C125.96 109.979 125.145 110.046 124.567 110.622Z" fill="#364153"/>
<path d="M118.825 114.462C120.273 113.314 121.951 113.576 123.181 115.128C124.411 116.679 124.284 118.373 122.835 119.521C121.387 120.669 119.7 120.414 118.47 118.862C117.24 117.31 117.377 115.609 118.825 114.462ZM119.496 115.308C118.734 115.912 118.778 116.933 119.598 117.968C120.418 119.002 121.403 119.278 122.165 118.675C122.926 118.071 122.882 117.049 122.062 116.015C121.242 114.98 120.258 114.704 119.496 115.308Z" fill="#364153"/>
<path d="M118.909 122.329L117.743 123.044L116.407 120.865L114.084 122.289L115.421 124.468L114.255 125.183L111.068 119.987L112.234 119.272L113.539 121.399L115.861 119.975L114.556 117.847L115.722 117.132L118.909 122.329Z" fill="#364153"/>
<path d="M107.424 124.17L108.708 123.573C110.123 122.916 111.188 123.215 111.724 124.368C112.26 125.522 111.801 126.529 110.387 127.186L107.862 128.359L105.293 122.83L106.534 122.254L107.424 124.17ZM107.854 125.095L108.673 126.858L109.826 126.322C110.523 125.998 110.75 125.548 110.472 124.95C110.199 124.362 109.704 124.235 109.007 124.559L107.854 125.095Z" fill="#364153"/>
<path d="M105.449 129.373L104.15 129.803L102.563 125.007L100.9 125.557C102.166 129.689 101.777 130.652 100.455 131.089C100.262 131.153 100.095 131.183 99.9351 131.198L99.6222 130.252C99.6829 130.245 99.7664 130.23 99.8462 130.204C100.598 129.955 100.664 129.162 99.3526 124.97L103.534 123.586L105.449 129.373Z" fill="#364153"/>
<path d="M94.9125 125.922C96.4864 125.592 97.5296 126.183 97.8565 127.745L98.3236 129.977C98.471 130.682 98.633 131.163 98.8056 131.519L97.5959 131.772C97.483 131.526 97.3981 131.237 97.3391 130.955L97.3156 130.96C97.0138 131.685 96.4131 132.154 95.5087 132.344C94.2637 132.604 93.3311 132.015 93.1173 130.993C92.9133 130.018 93.356 129.263 95.0144 128.634C95.4787 128.451 96.1687 128.233 96.5866 128.121L96.5375 127.886C96.3531 127.005 95.7978 126.754 95.1165 126.896C94.3766 127.051 94.0633 127.546 94.1608 128.188L92.8453 128.463C92.668 127.323 93.3034 126.258 94.9125 125.922ZM96.83 129.284L96.7685 128.99C96.3741 129.097 95.8109 129.277 95.4263 129.431C94.5983 129.751 94.3353 130.076 94.4484 130.616C94.559 131.145 95.0086 131.418 95.6898 131.276C96.1009 131.19 96.4732 130.977 96.6868 130.65C96.9351 130.255 96.9627 129.918 96.83 129.284Z" fill="#364153"/>
<path d="M91.8634 132.922L90.5013 133.049L90.264 130.504L87.5518 130.757L87.7891 133.301L86.427 133.428L85.861 127.359L87.2231 127.232L87.4548 129.717L90.1671 129.464L89.9353 126.979L91.2974 126.852L91.8634 132.922Z" fill="#364153"/>
<path d="M81.5124 127.233C83.3597 127.283 84.4775 128.562 84.4239 130.541C84.3702 132.52 83.1849 133.737 81.3375 133.687C79.4902 133.637 78.3605 132.358 78.4141 130.378C78.4677 128.399 79.6651 127.183 81.5124 127.233ZM81.4832 128.313C80.5115 128.286 79.8893 129.098 79.8536 130.417C79.8178 131.737 80.3952 132.581 81.3668 132.607C82.3384 132.633 82.9606 131.822 82.9964 130.502C83.0322 129.183 82.4548 128.339 81.4832 128.313Z" fill="#364153"/>
<path d="M76.8613 133.381L75.4965 133.176L75.8382 130.897C75.9646 130.055 76.1165 129.204 76.2784 128.367L76.2547 128.363C75.7812 129.093 75.2448 129.838 74.6883 130.555L72.9332 132.791L70.9276 132.491L71.8317 126.462L73.1965 126.667L72.853 128.957C72.7284 129.788 72.5765 130.639 72.4027 131.474L72.4264 131.478C72.9118 130.75 73.4481 130.005 74.0047 129.288L75.7598 127.051L77.7654 127.352L76.8613 133.381Z" fill="#364153"/>
<path d="M68.8958 131.047L69.6937 131.269L68.9013 134.125L67.5831 133.759L68.0964 131.909L63.448 130.619L65.0777 124.745L66.3959 125.111L65.0453 129.979L67.5776 130.681L68.9282 125.813L70.2464 126.179L68.8958 131.047Z" fill="#364153"/>
<path d="M61.0629 123.076C62.7716 123.78 63.3615 125.372 62.6074 127.203C61.8533 129.034 60.3129 129.749 58.6041 129.045C56.8954 128.342 56.2944 126.744 57.0485 124.914C57.8025 123.083 59.3541 122.372 61.0629 123.076ZM60.6515 124.075C59.7528 123.704 58.8827 124.241 58.38 125.462C57.8772 126.683 58.1167 127.677 59.0154 128.047C59.9142 128.417 60.7843 127.88 61.287 126.659C61.7897 125.439 61.5503 124.445 60.6515 124.075Z" fill="#364153"/>
<path d="M55.0112 127.492L53.8992 126.85L55.3393 124.356C55.7114 123.712 56.0938 123.074 56.4866 122.441L56.4762 122.435C55.8963 122.96 55.3017 123.462 54.6906 123.968L52.3092 125.932L50.8959 125.116L51.4171 122.078C51.5501 121.295 51.6876 120.529 51.8517 119.765L51.831 119.753C51.4797 120.409 51.1181 121.06 50.746 121.704L49.3059 124.198L48.2044 123.562L51.2526 118.283L53.1336 119.369L52.6564 122.211C52.5174 123.004 52.3352 123.799 52.1442 124.562L52.165 124.574C52.7301 124.027 53.3384 123.478 53.9554 122.961L56.168 121.121L58.0594 122.213L55.0112 127.492Z" fill="#364153"/>
<path d="M48.0349 112.657C49.9061 114.16 49.9973 116.403 48.269 118.555C46.5407 120.707 44.3398 121.11 42.4873 119.622C40.8781 118.329 40.5835 116.569 41.668 114.931L42.8188 115.855C42.2115 116.861 42.3838 117.892 43.2913 118.621C44.3018 119.432 45.5597 119.227 46.6901 118.087L44.0517 115.968L44.8332 114.995L47.4716 117.114C48.34 115.765 48.2506 114.477 47.2308 113.658C46.3327 112.937 45.2887 112.991 44.4888 113.795L43.3567 112.886C44.5569 111.526 46.3976 111.342 48.0349 112.657Z" fill="#364153"/>
<path d="M134.829 102.607C134.653 102.82 134.338 102.85 134.125 102.674C133.912 102.499 133.882 102.183 134.058 101.97L134.829 102.607ZM138.05 90.0848C138.232 89.8772 138.548 89.8564 138.756 90.0384L142.14 93.0047C142.347 93.1867 142.368 93.5026 142.186 93.7103C142.004 93.9179 141.688 93.9387 141.48 93.7567L138.472 91.12L135.836 94.1279C135.654 94.3356 135.338 94.3564 135.13 94.1743C134.922 93.9923 134.902 93.6764 135.084 93.4687L138.05 90.0848ZM134.058 101.97C136.195 99.3803 137.165 96.4976 137.598 94.2518C137.815 93.1302 137.897 92.1724 137.925 91.4973C137.938 91.1599 137.939 90.8937 137.936 90.7136C137.934 90.6235 137.932 90.5551 137.93 90.51C137.929 90.4875 137.928 90.4709 137.928 90.4603C137.928 90.455 137.927 90.4513 137.927 90.4491C137.927 90.448 137.927 90.4473 137.927 90.447C137.927 90.4468 137.927 90.4468 137.927 90.4468C137.927 90.4468 137.927 90.4469 137.927 90.4469C137.927 90.4471 137.927 90.4472 138.426 90.4144C138.925 90.3816 138.925 90.3818 138.925 90.382C138.925 90.3821 138.925 90.3824 138.925 90.3826C138.925 90.383 138.925 90.3836 138.925 90.3842C138.925 90.3855 138.925 90.3871 138.925 90.3892C138.926 90.3933 138.926 90.399 138.926 90.4062C138.927 90.4207 138.928 90.4414 138.929 90.468C138.932 90.5212 138.934 90.5982 138.936 90.6971C138.939 90.8948 138.938 91.1803 138.924 91.5385C138.894 92.2544 138.808 93.2627 138.58 94.4413C138.125 96.796 137.104 99.8504 134.829 102.607L134.058 101.97Z" fill="#364153"/>
<path d="M98.9313 12.3262C99.1994 12.3922 99.3632 12.6631 99.2972 12.9313C99.2311 13.1994 98.9602 13.3632 98.6921 13.2971L98.9313 12.3262ZM74.6677 12.0551C74.4418 11.8963 74.3873 11.5845 74.5461 11.3585L77.1332 7.67657C77.2919 7.45062 77.6038 7.39616 77.8298 7.55492C78.0557 7.71368 78.1102 8.02554 77.9514 8.25148L75.6517 11.5243L78.9246 13.824C79.1505 13.9828 79.205 14.2946 79.0463 14.5206C78.8875 14.7465 78.5756 14.801 78.3497 14.6422L74.6677 12.0551ZM98.6921 13.2971C92.4148 11.7504 86.497 11.4606 82.1458 11.5585C79.9712 11.6075 78.1906 11.7532 76.9555 11.8864C76.3381 11.953 75.8571 12.0165 75.5317 12.063C75.369 12.0863 75.2452 12.1054 75.1627 12.1186C75.1214 12.1251 75.0904 12.1302 75.0701 12.1336C75.0599 12.1353 75.0524 12.1366 75.0476 12.1374C75.0452 12.1378 75.0434 12.1381 75.0424 12.1383C75.0418 12.1384 75.0415 12.1385 75.0413 12.1385C75.0412 12.1385 75.0412 12.1385 75.0411 12.1385C75.0411 12.1385 75.0412 12.1385 74.9552 11.646C74.8692 11.1534 74.8693 11.1534 74.8695 11.1534C74.8696 11.1534 74.8699 11.1533 74.8701 11.1533C74.8706 11.1532 74.8713 11.1531 74.8721 11.1529C74.8738 11.1526 74.8761 11.1522 74.8791 11.1517C74.8851 11.1507 74.8939 11.1492 74.9053 11.1473C74.9281 11.1435 74.9615 11.138 75.0053 11.131C75.0929 11.1171 75.2219 11.0972 75.39 11.0731C75.7262 11.025 76.2186 10.9601 76.8483 10.8922C78.1076 10.7564 79.9167 10.6085 82.1233 10.5588C86.5345 10.4595 92.5449 10.7525 98.9313 12.3262L98.6921 13.2971Z" fill="#364153"/>
<path d="M25.8949 92.3186C25.813 92.0549 25.9604 91.7748 26.2242 91.6929C26.4879 91.611 26.768 91.7584 26.8499 92.0221L25.8949 92.3186ZM36.0618 106.412C36.0861 106.687 35.8829 106.93 35.6078 106.954L31.1253 107.351C30.8502 107.375 30.6075 107.172 30.5832 106.897C30.5589 106.622 30.7621 106.379 31.0372 106.355L35.0216 106.002L34.6692 102.018C34.6448 101.743 34.8481 101.5 35.1232 101.476C35.3982 101.452 35.641 101.655 35.6653 101.93L36.0618 106.412ZM26.8499 92.0221C28.2086 96.3983 30.4699 99.9155 32.3963 102.341C33.3587 103.553 34.2352 104.49 34.8693 105.122C35.1863 105.438 35.4426 105.678 35.6184 105.838C35.7063 105.918 35.7741 105.977 35.8193 106.017C35.8419 106.037 35.8589 106.051 35.8699 106.06C35.8754 106.065 35.8794 106.069 35.8819 106.071C35.8832 106.072 35.884 106.072 35.8845 106.073C35.8847 106.073 35.8849 106.073 35.8849 106.073C35.885 106.073 35.8849 106.073 35.8849 106.073C35.8849 106.073 35.8848 106.073 35.5637 106.456C35.2427 106.84 35.2426 106.84 35.2424 106.84C35.2423 106.839 35.2421 106.839 35.242 106.839C35.2416 106.839 35.2412 106.838 35.2406 106.838C35.2396 106.837 35.2381 106.836 35.2362 106.834C35.2325 106.831 35.2273 106.827 35.2205 106.821C35.2071 106.809 35.1876 106.793 35.1625 106.771C35.1122 106.727 35.0392 106.663 34.946 106.578C34.7595 106.408 34.492 106.158 34.1633 105.83C33.5061 105.175 32.603 104.21 31.6131 102.963C29.6349 100.472 27.3005 96.8461 25.8949 92.3186L26.8499 92.0221Z" fill="#364153"/>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4997 12.6674C19.4997 12.157 19.4572 11.6466 19.3722 11.1466H12.1553V14.0319H16.2898C16.1198 14.9589 15.5671 15.7922 14.7593 16.313V18.1879H17.2252C18.6707 16.8859 19.4997 14.9589 19.4997 12.6674Z" fill="#4285F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1556 19.9992C14.2175 19.9992 15.9606 19.3326 17.2254 18.1868L14.7596 16.3119C14.0687 16.7702 13.1865 17.0306 12.1556 17.0306C10.1574 17.0306 8.46739 15.7078 7.86155 13.937H5.32129V15.8744C6.61799 18.4056 9.26454 19.9992 12.1556 19.9992Z" fill="#34A853"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86133 13.9374C7.54247 13.0103 7.54247 11.9999 7.86133 11.0625V9.1355H5.32107C4.22631 11.25 4.22631 13.7499 5.32107 15.8643L7.86133 13.9374Z" fill="#FBBC04"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1556 7.96906C13.2503 7.94823 14.3026 8.35446 15.0891 9.09401L17.2786 6.94828C15.8862 5.6775 14.0581 4.97962 12.1556 5.00045C9.26454 5.00045 6.61799 6.60454 5.32129 9.13568L7.86155 11.0731C8.46739 9.29192 10.1574 7.96906 12.1556 7.96906Z" fill="#EA4335"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,16 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1815_12873)">
<mask id="mask0_1815_12873" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="5" width="16" height="16">
<path d="M19.0752 5.5H4.0752V20.5H19.0752V5.5Z" fill="white"/>
</mask>
<g mask="url(#mask0_1815_12873)">
<path d="M4.0752 12.7C4.0752 9.30589 4.0752 7.60883 5.12961 6.55442C6.18403 5.5 7.88109 5.5 11.2752 5.5H11.8752C15.2693 5.5 16.9664 5.5 18.0208 6.55442C19.0752 7.60883 19.0752 9.30589 19.0752 12.7V13.3C19.0752 16.6941 19.0752 18.3912 18.0208 19.4456C16.9664 20.5 15.2693 20.5 11.8752 20.5H11.2752C7.88109 20.5 6.18403 20.5 5.12961 19.4456C4.0752 18.3912 4.0752 16.6941 4.0752 13.3V12.7Z" fill="#0077FF"/>
<path d="M12.0564 16.3062C8.63769 16.3062 6.6877 13.9625 6.60645 10.0625H8.31895C8.3752 12.925 9.63768 14.1375 10.6377 14.3875V10.0625H12.2502V12.5312C13.2377 12.425 14.2751 11.3 14.6251 10.0625H16.2376C15.9689 11.5875 14.8439 12.7125 14.0439 13.175C14.8439 13.55 16.1252 14.5312 16.6127 16.3062H14.8377C14.4564 15.1187 13.5065 14.2 12.2502 14.075V16.3062H12.0564Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1815_12873">
<rect width="15.15" height="15" fill="white" transform="translate(4 5.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12.5" r="8.5" fill="#F23F1F"/>
<path d="M12.953 8.89273H12.237C11.0098 8.89273 10.3961 9.54333 10.3961 10.5192C10.3961 11.6036 10.8052 12.1458 11.7257 12.7963L12.4416 13.3385L10.3961 16.7H8.75977L10.7029 13.6638C9.57795 12.7963 8.96431 12.0373 8.96431 10.6277C8.96431 8.89273 10.0893 7.69995 12.237 7.69995H14.3848V16.7H12.953V8.89273Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

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

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