начал делать создание приема

This commit is contained in:
Андрей Дувакин 2025-06-01 13:04:46 +05:00
parent b0e654a367
commit 88ee83047d
27 changed files with 803 additions and 134 deletions

View File

@ -20,6 +20,7 @@
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-quill": "^2.0.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"validator": "^13.12.0" "validator": "^13.12.0"
@ -1515,6 +1516,15 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.18", "version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
@ -1906,7 +1916,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.0", "call-bind-apply-helpers": "^1.0.0",
@ -1938,7 +1947,6 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -1984,6 +1992,15 @@
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2146,6 +2163,26 @@
} }
} }
}, },
"node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -2157,7 +2194,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0", "es-define-property": "^1.0.0",
@ -2175,7 +2211,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.0.1", "define-data-property": "^1.0.1",
@ -2660,6 +2695,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2667,6 +2714,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -2832,7 +2885,6 @@
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -2975,7 +3027,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0" "es-define-property": "^1.0.0"
@ -3110,6 +3161,22 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -3232,7 +3299,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
"integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@ -3337,7 +3403,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@ -3615,6 +3680,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3732,11 +3803,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": { "node_modules/object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3885,6 +3971,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -4008,6 +4100,34 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/rc-cascader": { "node_modules/rc-cascader": {
"version": "3.33.1", "version": "3.33.1",
"resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.1.tgz", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.1.tgz",
@ -4651,6 +4771,21 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-quill": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
"license": "MIT",
"dependencies": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@ -4762,7 +4897,6 @@
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind": "^1.0.8", "call-bind": "^1.0.8",
@ -4951,7 +5085,6 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.1.4", "define-data-property": "^1.1.4",
@ -4969,7 +5102,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.1.4", "define-data-property": "^1.1.4",

View File

@ -22,6 +22,7 @@
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-quill": "^2.0.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.1.1", "react-router-dom": "^7.1.1",
"validator": "^13.12.0" "validator": "^13.12.0"

View File

@ -0,0 +1,23 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import CONFIG from "../Core/сonfig.js";3
export const appointmentTypesApi = createApi({
reducerPath: 'appointmentTypesApi',
baseQuery: fetchBaseQuery({
baseUrl: CONFIG.BASE_URL,
prepareHeaders: (headers) => {
const token = localStorage.getItem('access_token');
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
}
}),
tagsTypes: ['AppointmentTypes'],
endpoints: (builder) => ({
getAppointmentTypes: builder.query({
query: () => '/appointment_types/',
providesTags: ['AppointmentTypes'],
}),
}),
});
export const {useGetAppointmentTypesQuery} = appointmentTypesApi;

View File

@ -1,7 +1,6 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react"; import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import CONFIG from "../Core/сonfig.js"; import CONFIG from "../Core/сonfig.js";
export const appointmentsApi = createApi({ export const appointmentsApi = createApi({
reducerPath: 'appointmentsApi', reducerPath: 'appointmentsApi',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
@ -10,18 +9,36 @@ export const appointmentsApi = createApi({
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
if (token) headers.set('Authorization', `Bearer ${token}`); if (token) headers.set('Authorization', `Bearer ${token}`);
return headers; return headers;
} },
}), }),
tagTypes: ['Appointment'], tagTypes: ['Appointment'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getAppointments: builder.query({ getAppointments: builder.query({
query: () => '/appointments/', query: () => '/appointments/',
providesTags: ['Appointment'], providesTags: ['Appointment'],
refetchOnMountOrArgChange: 5 refetchOnMountOrArgChange: 5,
}),
createAppointment: builder.mutation({
query: (data) => ({
url: '/appointments/',
method: 'POST',
body: data,
}),
invalidatesTags: ['Appointment'],
}),
updateAppointment: builder.mutation({
query: ({ id, data }) => ({
url: `/appointments/${id}/`,
method: 'PUT',
body: data,
}),
invalidatesTags: ['Appointment'],
}), }),
}), }),
}); });
export const { export const {
useGetAppointmentsQuery, useGetAppointmentsQuery,
useCreateAppointmentMutation,
useUpdateAppointmentMutation,
} = appointmentsApi; } = appointmentsApi;

View File

@ -1,7 +1,6 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react"; import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import CONFIG from "../Core/сonfig.js"; import CONFIG from "../Core/сonfig.js";
export const scheduledAppointmentsApi = createApi({ export const scheduledAppointmentsApi = createApi({
reducerPath: 'scheduledAppointmentsApi', reducerPath: 'scheduledAppointmentsApi',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
@ -10,16 +9,35 @@ export const scheduledAppointmentsApi = createApi({
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
if (token) headers.set('Authorization', `Bearer ${token}`); if (token) headers.set('Authorization', `Bearer ${token}`);
return headers; return headers;
} },
}), }),
tagTypes: ['ScheduledAppointment'], tagTypes: ['ScheduledAppointment'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getScheduledAppointments: builder.query({ getScheduledAppointments: builder.query({
query: () => `/scheduledAppointments`, query: () => `/scheduled_appointments/`,
providesTags: ['ScheduledAppointment'],
}),
createScheduledAppointment: builder.mutation({
query: (data) => ({
url: '/scheduled_appointments/',
method: 'POST',
body: data,
}),
invalidatesTags: ['ScheduledAppointment'],
}),
updateScheduledAppointment: builder.mutation({
query: ({ id, data }) => ({
url: `/scheduled_appointments/${id}/`,
method: 'PUT',
body: data,
}),
invalidatesTags: ['ScheduledAppointment'],
}), }),
}), }),
}); });
export const { export const {
useGetScheduledAppointmentsQuery, useGetScheduledAppointmentsQuery,
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation,
} = scheduledAppointmentsApi; } = scheduledAppointmentsApi;

View File

@ -1,4 +1,4 @@
import {Card, Modal, Popconfirm, Tooltip} from "antd"; import {Card, Popconfirm, Tooltip} from "antd";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {DeleteOutlined, EditOutlined, EyeOutlined} from "@ant-design/icons"; import {DeleteOutlined, EditOutlined, EyeOutlined} from "@ant-design/icons";
import {useState} from "react"; import {useState} from "react";

View File

@ -1,74 +1,130 @@
import {Button, Tabs, Typography} from "antd"; import {Button, FloatButton, Result, Tabs, Typography} from "antd";
import {Splitter} from "antd"; import {Splitter} from "antd";
import { import {CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PlusOutlined} from "@ant-design/icons";
CalendarOutlined, TableOutlined, MenuFoldOutlined, MenuUnfoldOutlined,
} from "@ant-design/icons";
import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx"; import AppointmentsCalendarTab from "./Components/AppointmentCalendarTab/AppointmentsCalendarTab.jsx";
import AppointmentsTableTab from "./Components/AppointmentTableTab/AppointmentsTableTab.jsx"; import AppointmentsTableTab from "./Components/AppointmentTableTab/AppointmentsTableTab.jsx";
import useAppointmentsUI from "./useAppointmentsUI.js"; import useAppointmentsUI from "./useAppointmentsUI.js";
import useAppointments from "./useAppointments.js";
import dayjs from 'dayjs';
import LoadingIndicator from "../../Widgets/LoadingIndicator.jsx";
import AppointmentFormModal
from "./Components/AppointmentCalendarTab/Components/AppointmentFormModal/AppointmentFormModal.jsx";
const AppointmentsPage = () => { const AppointmentsPage = () => {
const appointmentsPageUI = useAppointmentsUI(); const appointmentsData = useAppointments();
const appointmentsPageUI = useAppointmentsUI(appointmentsData.appointments, appointmentsData.scheduledAppointments);
const items = [{ const items = [
key: "1", {
label: "Календарь приемов", key: "1",
children: <AppointmentsCalendarTab/>, label: "Календарь приемов",
icon: <CalendarOutlined/>, children: <AppointmentsCalendarTab/>,
}, { icon: <CalendarOutlined/>,
key: "2", },
label: "Таблица приемов", {
children: <AppointmentsTableTab/>, key: "2",
icon: <TableOutlined/>, label: "Таблица приемов",
},]; children: <AppointmentsTableTab/>,
icon: <TableOutlined/>,
},
];
if (appointmentsData.isError) return (
<Result
status="error"
title="Ошибка"
subTitle="Произошла ошибка в работе страницы"
/>
);
return ( return (
<> <>
<Splitter {appointmentsData.isLoading ? (
style={appointmentsPageUI.splitterStyle} <LoadingIndicator/>
min={200} ) : (
max={400} <>
initial={appointmentsPageUI.siderWidth} <Splitter
onChange={appointmentsPageUI.setSiderWidth} style={appointmentsPageUI.splitterStyle}
> min={200}
<Splitter.Panel max={400}
style={appointmentsPageUI.splitterContentPanelStyle} initial={appointmentsPageUI.siderWidth}
defaultSize="80%" onChange={appointmentsPageUI.setSiderWidth}
min="25%"
max="90%"
>
<Tabs defaultActiveKey="1" items={items}/>
</Splitter.Panel>
{appointmentsPageUI.showSplitterPanel && (
<Splitter.Panel
style={appointmentsPageUI.splitterSiderPanelStyle}
defaultSize="20%"
min="20%"
max="75%"
> >
<Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}> <Splitter.Panel
Предстоящие события style={appointmentsPageUI.splitterContentPanelStyle}
</Typography.Title> defaultSize="80%"
<p>Здесь будут предстоящие приемы...</p> min="25%"
</Splitter.Panel> max="90%"
)} >
</Splitter> <Tabs defaultActiveKey="1" items={items}/>
<div </Splitter.Panel>
style={appointmentsPageUI.siderButtonContainerStyle}
onMouseEnter={appointmentsPageUI.handleHoverSider} {appointmentsPageUI.showSplitterPanel && (
onMouseLeave={appointmentsPageUI.handleLeaveSider} <Splitter.Panel
> style={appointmentsPageUI.splitterSiderPanelStyle}
<Button defaultSize="20%"
type="primary" min="20%"
onClick={appointmentsPageUI.handleToggleSider} max="75%"
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>} >
style={appointmentsPageUI.siderButtonStyle} <Typography.Title level={3} style={appointmentsPageUI.siderTitleStyle}>
> Предстоящие события
{appointmentsPageUI.siderButtonText} </Typography.Title>
</Button> {appointmentsPageUI.upcomingEvents.length ? (
</div> <ul>
{appointmentsPageUI.upcomingEvents.map(app => (
<li key={app.id}>
{dayjs(app.appointment_datetime || app.scheduled_datetime)
.tz('Europe/Moscow')
.format('DD.MM.YYYY HH:mm')} -
{app.appointment_datetime ? 'Прием' : 'Запланировано'}
</li>
))}
</ul>
) : (
<p>Нет предстоящих событий</p>
)}
</Splitter.Panel>
)}
</Splitter>
<div
style={appointmentsPageUI.siderButtonContainerStyle}
onMouseEnter={appointmentsPageUI.handleHoverSider}
onMouseLeave={appointmentsPageUI.handleLeaveSider}
>
<Button
type="primary"
onClick={appointmentsPageUI.handleToggleSider}
icon={appointmentsPageUI.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
style={appointmentsPageUI.siderButtonStyle}
>
{appointmentsPageUI.siderButtonText}
</Button>
</div>
<FloatButton.Group
trigger={"hover"}
type="primary"
icon={<PlusOutlined/>}
tooltip={"Создать"}
>
<FloatButton
icon={<PlusOutlined/>}
onClick={appointmentsPageUI.openCreateAppointmentModal}
tooltip={"Прием"}
/>
<FloatButton
icon={<CalendarOutlined/>}
onClick={appointmentsPageUI.openCreateScheduledAppointmentModal}
tooltip={"Запланированный прием"}
/>
</FloatButton.Group>
<AppointmentFormModal
visible={appointmentsPageUI.modalVisible}
onCancel={appointmentsPageUI.handleCloseModal}
onSubmit={appointmentsData.handleSubmitModal}
/>
</>
)}
</> </>
); );
}; };

View File

@ -1,25 +1,52 @@
import {Calendar} from "antd"; import {Calendar, Modal, Form, Input, DatePicker, Button} from "antd";
import 'dayjs/locale/ru'; import 'dayjs/locale/ru';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import CalendarCell from "../../../../Widgets/CalendarCell.jsx"; import CalendarCell from "../../../../Widgets/CalendarCell.jsx";
import useAppointments from "../../useAppointments.js"; import useAppointments from "../../useAppointments.js";
import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js"; import useAppointmentCalendarUI from "./useAppointmentCalendarUI.js";
import {
closeModal,
openModal,
setSelectedAppointment,
setSelectedScheduledAppointment,
} from "../../../../../Redux/Slices/appointmentsSlice.js"
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Almaty');
const AppointmentsCalendarTab = () => { const AppointmentsCalendarTab = () => {
const appointmentsData = useAppointments(); const appointmentsData = useAppointments();
const appointmentsCalendarUI = useAppointmentCalendarUI(appointmentsData.appointments, appointmentsData.scheduledAppointments); const appointmentsCalendarUI = useAppointmentCalendarUI(
appointmentsData.appointments,
appointmentsData.scheduledAppointments
);
const dateCellRender = (value) => { const dateCellRender = (value) => {
const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(appointmentsData.appointments, value); const appointmentsForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(appointmentsData.scheduledAppointments, value); appointmentsData.appointments,
value
);
const scheduledForDate = appointmentsCalendarUI.getAppointmentsByListAndDate(
appointmentsData.scheduledAppointments,
value,
true
);
return ( return (
<CalendarCell <CalendarCell
appointments={appointmentsForDate} appointments={appointmentsForDate}
scheduledAppointments={scheduledForDate} scheduledAppointments={scheduledForDate}
onCellClick={() => { onCellClick={() => appointmentsCalendarUI.onSelect(value)}
}} onItemClick={(appointment) => {
onItemClick={() => { if (appointment.appointment_datetime) {
dispatch(setSelectedAppointment(appointment));
} else {
dispatch(setSelectedScheduledAppointment(appointment));
}
dispatch(openModal());
}} }}
/> />
); );

View File

@ -0,0 +1,120 @@
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import dayjs from "dayjs";
import {Button, DatePicker, Form, InputNumber, Modal, Result, Select} from "antd";
import useAppointmentFormModal from "./useAppointmentFormModal.js";
import useAppointmentFormModalUI from "./useAppointmentFormModalUI.js";
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator.jsx";
import {DefaultModalPropType} from "../../../../../../../Types/defaultModalPropType.js";
const AppointmentFormModal = ({visible, onCancel, onSubmit}) => {
const appointmentFormModalData = useAppointmentFormModal();
const appointmentFormModalUI = useAppointmentFormModalUI(visible, onCancel, onSubmit);
if (appointmentFormModalData.isError) {
return (
<Result
status="error"
title="Ошибка"
subTitle="Произошла ошибка в работе страницы"
/>
);
}
return (
<>
{appointmentFormModalData.isLoading ? (
<LoadingIndicator/>
) : (
<Modal
title={appointmentFormModalUI.selectedAppointment ? "Редактировать прием" : "Создать прием"}
open={appointmentFormModalUI.modalVisible}
onCancel={appointmentFormModalUI.onCancel}
footer={null}
>
<Form
form={appointmentFormModalUI.form}
onFinish={appointmentFormModalUI.onFinish}
initialValues={
appointmentFormModalUI.selectedAppointment
? {
patient_id: appointmentFormModalUI.selectedAppointment.patient_id,
type_id: appointmentFormModalUI.selectedAppointment.type_id,
appointmentTime: dayjs(appointmentFormModalUI.selectedAppointment.appointment_datetime).tz('Europe/Moscow'),
days_until_the_next_appointment: appointmentFormModalUI.selectedAppointment.days_until_the_next_appointment,
results: appointmentFormModalUI.selectedAppointment.results,
}
: {}
}
layout="vertical"
>
<Form.Item
name="patient_id"
label="Пациент"
rules={[{required: true, message: 'Выберите пациента'}]}
>
<Select
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
option.children.toLowerCase().includes(input.toLowerCase())
}
placeholder="Выберите пациента"
>
{appointmentFormModalData.patients.map(patient => (
<Select.Option key={patient.id} value={patient.id}>
{patient.last_name} {patient.first_name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="type_id"
label="Тип приема"
rules={[{required: true, message: 'Выберите тип приема'}]}
>
<Select placeholder="Выберите тип приема">
{appointmentFormModalData.appointmentTypes.map(type => (
<Select.Option key={type.id} value={type.id}>
{type.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="appointment_datetime"
label="Время приема"
rules={[{required: true, message: 'Выберите время'}]}
>
<DatePicker defaultValue={dayjs().tz('Asia/Almaty')} showTime format="DD.MM.YYYY HH:mm"
style={{width: '100%'}}/>
</Form.Item>
<Form.Item
name="days_until_the_next_appointment"
label="Дней до следующего приема"
rules={[{type: 'number', min: 0, message: 'Введите неотрицательное число'}]}
>
<InputNumber min={0} style={{width: '100%'}}/>
</Form.Item>
<Form.Item
name="results"
label="Результаты приема"
>
<ReactQuill theme="snow"></ReactQuill>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{appointmentFormModalUI.selectedAppointment ? 'Сохранить' : 'Создать'}
</Button>
</Form.Item>
</Form>
</Modal>
)}
</>
);
};
AppointmentFormModal.propTypes = DefaultModalPropType;
export default AppointmentFormModal;

View File

@ -0,0 +1,28 @@
import {useGetPatientsQuery} from "../../../../../../../Api/patientsApi.js";
import {useGetAppointmentTypesQuery} from "../../../../../../../Api/appointmentTypesApi.js";
const useAppointmentFormModal = () => {
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000,
});
const {
data: appointmentTypes = [],
isLoading: isLoadingAppointmentTypes,
isError: isErrorAppointmentTypes
} = useGetAppointmentTypesQuery(undefined, {
pollingInterval: 20000,
});
return {
patients,
appointmentTypes,
isLoading: isLoadingPatients || isLoadingAppointmentTypes,
isError: isErrorPatients || isErrorAppointmentTypes,
};
};
export default useAppointmentFormModal;

View File

@ -0,0 +1,55 @@
import {Form, message, notification} from "antd";
import { useDispatch, useSelector } from "react-redux";
import {closeModal} from "../../../../../../../Redux/Slices/appointmentsSlice.js";
import {useEffect} from "react";
import dayjs from "dayjs";
const useAppointmentFormModalUI = (visible, onCancel, onSubmit) => {
const { modalVisible, selectedAppointment } = useSelector(state => state.appointmentsUI);
const [form] = Form.useForm();
useEffect(() => {
if (visible) {
form.resetFields();
if (selectedAppointment) {
form.setFieldsValue({
...selectedAppointment,
appointment_datetime: selectedAppointment.appointment_datetime ? dayjs(selectedAppointment.appointment_datetime, "YYYY-MM-DD HH:mm") : null,
});
}
}
}, []);
const handleOk = async () => {
try {
const values = await form.validateFields();
if (values.birthday) {
values.appointment_datetime = values.appointment_datetime.format("YYYY-MM-DD HH:mm");
}
onSubmit(values);
form.resetFields();
} catch (error) {
console.log("Validation Failed:", error);
notification.error({
message: "Ошибка валидации",
description: "Проверьте правильность заполнения полей.",
placement: "topRight",
});
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return {
form,
modalVisible,
selectedAppointment,
handleOk,
handleCancel,
};
};
export default useAppointmentFormModalUI;

View File

@ -0,0 +1,13 @@
import {useCreateAppointmentMutation, useUpdateAppointmentMutation} from "../../../../../Api/appointmentsApi.js";
import {
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation
} from "../../../../../Api/scheduledAppointmentsApi.js";
const useAppointmentCalendar = () => {
const [createAppointment] = useCreateAppointmentMutation();
const [updateAppointment] = useUpdateAppointmentMutation();
const [createScheduledAppointment] = useCreateScheduledAppointmentMutation();
const [updateScheduledAppointment] = useUpdateScheduledAppointmentMutation();
};

View File

@ -1,11 +1,24 @@
// useAppointmentCalendarUI.js
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import {Form, Grid, notification} from "antd";
import { import {
openModal, closeModal,
openModal, setSelectedAppointment,
setSelectedAppointments, setSelectedAppointments,
setSelectedDate setSelectedDate, setSelectedScheduledAppointment,
} from "../../../../../Redux/Slices/appointmentsSlice.js"; } from "../../../../../Redux/Slices/appointmentsSlice.js";
import {Grid} from "antd"; import {useCreateAppointmentMutation, useUpdateAppointmentMutation} from "../../../../../Api/appointmentsApi.js";
import {
useCreateScheduledAppointmentMutation,
useUpdateScheduledAppointmentMutation
} from "../../../../../Api/scheduledAppointmentsApi.js";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Europe/Moscow');
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
@ -15,9 +28,9 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
modalVisible, modalVisible,
selectedAppointments, selectedAppointments,
selectedAppointment, selectedAppointment,
selectedScheduledAppointment,
} = useSelector(state => state.appointmentsUI); } = useSelector(state => state.appointmentsUI);
const selectedDate = dayjs(useSelector(state => state.appointmentsUI.selectedDate)); const selectedDate = dayjs.tz(useSelector(state => state.appointmentsUI.selectedDate), 'Europe/Moscow');
const screens = useBreakpoint(); const screens = useBreakpoint();
const fullScreenCalendar = !screens.xs; const fullScreenCalendar = !screens.xs;
@ -25,29 +38,114 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
const calendarContainerStyle = {padding: 20}; const calendarContainerStyle = {padding: 20};
const onSelect = (date) => { const onSelect = (date) => {
const selectedDateStr = date.format('YYYY-MM-DD'); const selectedDateStr = date.tz('Europe/Moscow').format('YYYY-MM-DD');
dispatch(setSelectedDate(selectedDateStr)); dispatch(setSelectedDate(selectedDateStr));
console.log(appointments)
const appointmentsForDate = appointments.filter(app => const appointmentsForDate = appointments.filter(app =>
dayjs(app.appointment_datetime).format('YYYY-MM-DD') === selectedDateStr dayjs(app.appointment_datetime).tz('Europe/Moscow').format('YYYY-MM-DD') === selectedDateStr
); );
console.log(appointmentsForDate)
const scheduledForDate = scheduledAppointments.filter(app => const scheduledForDate = scheduledAppointments.filter(app =>
dayjs(app.scheduled_datetime).format('YYYY-MM-DD') === selectedDateStr dayjs(app.scheduled_datetime).tz('Europe/Moscow').format('YYYY-MM-DD') === selectedDateStr
); );
dispatch(setSelectedAppointments([...appointmentsForDate, ...scheduledForDate])); dispatch(setSelectedAppointments([...appointmentsForDate, ...scheduledForDate]));
dispatch(openModal()); dispatch(openModal());
}; };
const getAppointmentsByListAndDate = (list, value) => { const onFinish = async (values) => {
const date = value.format('YYYY-MM-DD'); try {
const appointmentTime = values.appointmentTime;
const isScheduled = !(selectedAppointment?.appointment_datetime);
const conflictingAppointments = isScheduled
? scheduledAppointments
: appointments;
const hasConflict = conflictingAppointments.some(app =>
dayjs(app.appointment_datetime || app.scheduled_datetime)
.tz('Europe/Moscow')
.isSame(appointmentTime, 'minute')
);
if (hasConflict) {
notification.error({
message: "Выбранное время уже занято",
description: "Выбранное время уже занято",
placement: "topRight",
});
return;
}
const data = {
patient: {
last_name: values.patientName.split(' ')[0] || '',
first_name: values.patientName.split(' ')[1] || '',
},
...(isScheduled
? { scheduled_datetime: appointmentTime.toISOString() }
: { appointment_datetime: appointmentTime.toISOString() }),
reason: values.reason,
};
if (selectedAppointment || selectedScheduledAppointment) {
if (selectedAppointment?.appointment_datetime) {
await updateAppointment({ id: selectedAppointment.id, data }).unwrap();
notification.success({
message: "Прием успешно обновлен",
description: "Прием успешно обновлен",
placement: "topRight",
});
} else {
await updateScheduledAppointment({ id: selectedScheduledAppointment.id, data }).unwrap();
notification.success({
message: "Запланированный прием успешно обновлен",
description: "Запланированный прием успешно обновлен",
placement: "topRight",
});
}
} else {
if (isScheduled) {
await createScheduledAppointment(data).unwrap();
notification.success({
message: "Запланированный прием успешно создан",
description: "Запланированный прием успешно создан",
placement: "topRight",
});
} else {
await createAppointment(data).unwrap();
notification.success({
message: "Прием успешно создан",
description: "Прием успешно создан",
placement: "topRight",
});
}
}
dispatch(closeModal());
form.resetFields();
} catch (error) {
notification.error({
message: "Ошибка при сохранении приема",
description: error.data?.message || "Не удалось сохранить прием",
placement: "topRight",
});
}
};
const handleCreateAppointment = () => {
dispatch(setSelectedAppointment(null));
dispatch(setSelectedScheduledAppointment(null));
form.resetFields();
dispatch(openModal());
};
const getAppointmentsByListAndDate = (list, value, isScheduled = false) => {
const date = value.tz('Europe/Moscow').format('YYYY-MM-DD');
return list.filter(app => return list.filter(app =>
dayjs(app.appointment_datetime).format('YYYY-MM-DD') === date dayjs(isScheduled ? app.scheduled_datetime : app.appointment_datetime)
.tz('Europe/Moscow')
.format('YYYY-MM-DD') === date
); );
} };
return { return {
selectedDate, selectedDate,

View File

@ -1,28 +1,74 @@
import {useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js"; import {useCreateAppointmentMutation, useGetAppointmentsQuery} from "../../../Api/appointmentsApi.js";
import {useGetScheduledAppointmentsQuery} from "../../../Api/scheduledAppointmentsApi.js";
import {useGetPatientsQuery} from "../../../Api/patientsApi.js";
import {notification} from "antd";
import {closeModal} from "../../../Redux/Slices/appointmentsSlice.js";
import {useDispatch} from "react-redux";
const useAppointments = () => { const useAppointments = () => {
const dispatch = useDispatch();
const [createAppointment] = useCreateAppointmentMutation();
const { const {
data: appointments = [], data: appointments = [],
isLoadingAppointments, isLoading: isLoadingAppointments,
isErrorAppointments, isError: isErrorAppointments,
} = useGetAppointmentsQuery(undefined, { } = useGetAppointmentsQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const { const {
data: scheduledAppointments = [], data: scheduledAppointments = [],
isLoadingScheduledAppointments, isLoading: isLoadingScheduledAppointments,
isErrorScheduledAppointments, isError: isErrorScheduledAppointments,
} = useGetAppointmentsQuery(undefined, { } = useGetScheduledAppointmentsQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
}); });
const {
data: patients = [],
isLoading: isLoadingPatients,
isError: isErrorPatients
} = useGetPatientsQuery(undefined, {
pollingInterval: 20000
});
const handleSubmitModal = async (values) => {
try {
const appointmentTime = values.appointmentTime;
const data = {
patient_id: values.patient_id,
type_id: values.type_id,
appointment_datetime: appointmentTime.toISOString(),
days_until_the_next_appointment: values.days_until_the_next_appointment,
results: values.results,
doctor_id: localStorage.getItem('doctor_id'),
};
await createAppointment(data).unwrap();
notification.success({
message: 'Прием создан',
description: 'Прием успешно создан',
placement: 'topRight',
});
dispatch(closeModal());
} catch (error) {
notification.error({
message: 'Ошибка при создании приема',
description: error.data?.message || 'Не удалось создать прием',
placement: 'topRight',
});
}
};
return { return {
patients,
appointments, appointments,
scheduledAppointments, scheduledAppointments,
isLoading: isLoadingAppointments || isLoadingScheduledAppointments, handleSubmitModal,
isError: isErrorAppointments || isErrorScheduledAppointments, isLoading: isLoadingAppointments || isLoadingScheduledAppointments || isLoadingPatients,
isError: isErrorAppointments || isErrorScheduledAppointments || isErrorPatients,
}; };
}; };

View File

@ -1,16 +1,18 @@
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {Grid} from "antd"; import {Grid} from "antd";
import {setHovered, toggleSider} from "../../../Redux/Slices/appointmentsSlice.js"; import {openModal, setHovered, toggleSider} from "../../../Redux/Slices/appointmentsSlice.js";
import {useEffect, useMemo} from "react"; import {useEffect, useMemo} from "react";
import dayjs from "dayjs";
const {useBreakpoint} = Grid; const {useBreakpoint} = Grid;
const useAppointmentsUI = () => { const useAppointmentsUI = (appointments, scheduledAppointments) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
collapsed, collapsed,
siderWidth, siderWidth,
hovered, hovered,
modalVisible,
} = useSelector(state => state.appointmentsUI); } = useSelector(state => state.appointmentsUI);
const screens = useBreakpoint(); const screens = useBreakpoint();
@ -36,7 +38,7 @@ const useAppointmentsUI = () => {
padding: hovered ? "0 20px" : "0", padding: hovered ? "0 20px" : "0",
overflow: "hidden", overflow: "hidden",
textAlign: "left", textAlign: "left",
transition: "width 0.3s ease, padding 0.3s ease", transition: "width 0.8s ease, padding 0.8s ease",
borderRadius: "4px 0 0 4px", borderRadius: "4px 0 0 4px",
}; };
@ -44,9 +46,18 @@ const useAppointmentsUI = () => {
const handleHoverSider = () => dispatch(setHovered(true)); const handleHoverSider = () => dispatch(setHovered(true));
const handleLeaveSider = () => dispatch(setHovered(false)); const handleLeaveSider = () => dispatch(setHovered(false));
const openCreateAppointmentModal = () => dispatch(openModal());
const handleCloseModal = () => dispatch(openModal(false));
const handleModalSubmit = () => dispatch(openModal(false));
const siderButtonText = useMemo(() => hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", [collapsed, hovered]); const siderButtonText = useMemo(() => hovered ? (collapsed ? "Показать предстоящие события" : "Скрыть предстоящие события") : "", [collapsed, hovered]);
const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]); const showSplitterPanel = useMemo(() => !collapsed && !screens.xs, [collapsed, screens]);
const upcomingEvents = [...appointments, ...scheduledAppointments]
.filter(app => dayjs(app.appointment_datetime || app.scheduled_datetime).isAfter(dayjs()))
.sort((a, b) => dayjs(a.appointment_datetime || a.scheduled_datetime) - dayjs(b.appointment_datetime || b.scheduled_datetime))
.slice(0, 5);
return { return {
collapsed, collapsed,
siderWidth, siderWidth,
@ -59,9 +70,14 @@ const useAppointmentsUI = () => {
siderTitleStyle, siderTitleStyle,
siderButtonContainerStyle, siderButtonContainerStyle,
siderButtonStyle, siderButtonStyle,
upcomingEvents,
modalVisible,
handleToggleSider, handleToggleSider,
handleHoverSider, handleHoverSider,
handleLeaveSider, handleLeaveSider,
openCreateAppointmentModal,
handleCloseModal,
handleModalSubmit,
}; };
}; };

View File

@ -189,8 +189,6 @@ const LensIssueFormModal = ({visible, onCancel, onSubmit}) => {
title: 'Подтверждение', content: ConfirmStep, title: 'Подтверждение', content: ConfirmStep,
}]; }];
console.log(steps[lensIssueFormModalUI.currentStep].title)
return ( return (
<Modal <Modal
title="Выдача линзы пациенту" title="Выдача линзы пациенту"

View File

@ -10,8 +10,6 @@ const useLensIssueForm = () => {
pollingInterval: 10000, pollingInterval: 10000,
}); });
console.log(lenses)
return { return {
patients, patients,
lenses, lenses,

View File

@ -6,9 +6,6 @@ import {closeModal} from "../../../Redux/Slices/lensIssuesSlice.js";
const useIssues = () => { const useIssues = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const {
selectedIssue,
} = useSelector(state => state.lensIssuesUI);
const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, { const {data: issues = [], isLoading, isError, error} = useGetLensIssuesQuery(undefined, {
pollingInterval: 20000, pollingInterval: 20000,
@ -39,7 +36,6 @@ const useIssues = () => {
isLoading, isLoading,
isError, isError,
error, error,
selectedIssue,
handleSubmitFormModal, handleSubmitFormModal,
}; };
}; };

View File

@ -28,6 +28,7 @@ const useIssuesUI = (issues) => {
endFilterDate, endFilterDate,
} = useSelector(state => state.lensIssuesUI); } = useSelector(state => state.lensIssuesUI);
useEffect(() => { useEffect(() => {
document.title = "Выдача линз"; document.title = "Выдача линз";
const cachedViewMode = getCachedInfo("viewModeIssues"); const cachedViewMode = getCachedInfo("viewModeIssues");

View File

@ -8,8 +8,8 @@ import usePatientFormUI from "./usePatientFormUI.js";
const {TextArea} = Input; const {TextArea} = Input;
const PatientFormModal = ({visible, onCancel, onSubmit, patient}) => { const PatientFormModal = ({visible, onCancel, onSubmit}) => {
const patientFormModalUI = usePatientFormUI(visible, onCancel, onSubmit, patient); const patientFormModalUI = usePatientFormUI(visible, onCancel, onSubmit);
return ( return (
<Modal <Modal

View File

@ -2,28 +2,31 @@ import {Form, notification} from "antd";
import {useEffect} from "react"; import {useEffect} from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import validator from "validator"; import validator from "validator";
import {useSelector} from "react-redux";
const usePatientFormUI = (visible, onCancel, onSubmit, patient) => { const usePatientFormUI = (visible, onCancel, onSubmit) => {
const {selectedPatient} = useSelector(state => state.patientsUI);
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
form.resetFields(); form.resetFields();
if (patient) { if (selectedPatient) {
form.setFieldsValue({ form.setFieldsValue({
...patient, ...selectedPatient,
birthday: patient.birthday ? dayjs(patient.birthday, "YYYY-MM-DD") : null, birthday: selectedPatient.birthday ? dayjs(selectedPatient.birthday, "YYYY-MM-DD") : null,
}); });
} }
} }
}, [visible, patient, form]); }, [visible, selectedPatient, form]);
const modalStyle = { const modalStyle = {
marginTop: 20, marginTop: 20,
marginBottom: 50, marginBottom: 50,
}; };
const modalTitle = patient ? "Редактировать пациента" : "Добавить пациента"; const modalTitle = selectedPatient ? "Редактировать пациента" : "Добавить пациента";
const emailValidator = (_, value) => { const emailValidator = (_, value) => {
if (value && !validator.isEmail(value)) { if (value && !validator.isEmail(value)) {

View File

@ -190,7 +190,6 @@ const PatientsPage = () => {
visible={patientsUI.isModalVisible} visible={patientsUI.isModalVisible}
onCancel={patientsUI.handleCloseModal} onCancel={patientsUI.handleCloseModal}
onSubmit={patientsData.handleModalSubmit} onSubmit={patientsData.handleModalSubmit}
patient={patientsUI.selectedPatient}
/> />
</div> </div>
); );

View File

@ -18,7 +18,6 @@ const usePatientsUI = (patients) => {
searchText, searchText,
sortOrder, sortOrder,
viewMode, viewMode,
selectedPatient,
isModalVisible, isModalVisible,
currentPage, currentPage,
pageSize, pageSize,
@ -87,7 +86,6 @@ const usePatientsUI = (patients) => {
searchText, searchText,
sortOrder, sortOrder,
viewMode, viewMode,
selectedPatient,
isModalVisible, isModalVisible,
currentPage, currentPage,
pageSize, pageSize,

View File

@ -53,7 +53,7 @@ const CalendarCell = ({appointments, scheduledAppointments, onCellClick, onItemC
> >
<Badge <Badge
status="success" status="success"
text={dayjs(app.scheduled_datetime).format('HH:mm') + ` ${app.patient.last_name} ${app.patient.first_name} `} text={dayjs(app.appointment_datetime).format('HH:mm') + ` ${app.patient.last_name} ${app.patient.first_name} `}
/> />
</Tag> </Tag>
</Tooltip> </Tooltip>

View File

@ -10,6 +10,8 @@ const initialState = {
modalVisible: false, modalVisible: false,
selectedAppointments: [], selectedAppointments: [],
selectedAppointment: null, selectedAppointment: null,
scheduledAppointments: [],
selectedScheduledAppointment: null,
}; };
const appointmentsSlice = createSlice({ const appointmentsSlice = createSlice({
@ -40,6 +42,12 @@ const appointmentsSlice = createSlice({
setSelectedAppointment: (state, action) => { setSelectedAppointment: (state, action) => {
state.selectedAppointment = action.payload; state.selectedAppointment = action.payload;
}, },
setScheduledAppointments: (state, action) => {
state.scheduledAppointments = action.payload;
},
setSelectedScheduledAppointment: (state, action) => {
state.selectedScheduledAppointment = action.payload;
},
} }
}); });
@ -52,6 +60,8 @@ export const {
closeModal, closeModal,
setSelectedAppointments, setSelectedAppointments,
setSelectedAppointment, setSelectedAppointment,
setSelectedScheduledAppointment,
setScheduledAppointments,
} = appointmentsSlice.actions; } = appointmentsSlice.actions;
export default appointmentsSlice.reducer; export default appointmentsSlice.reducer;

View File

@ -11,6 +11,8 @@ import lensIssuesReducer from "./Slices/lensIssuesSlice.js";
import {lensTypesApi} from "../Api/lensTypesApi.js"; import {lensTypesApi} from "../Api/lensTypesApi.js";
import {appointmentsApi} from "../Api/appointmentsApi.js"; import {appointmentsApi} from "../Api/appointmentsApi.js";
import appointmentsReducer from "./Slices/appointmentsSlice.js"; import appointmentsReducer from "./Slices/appointmentsSlice.js";
import {scheduledAppointmentsApi} from "../Api/scheduledAppointmentsApi.js";
import {appointmentTypesApi} from "../Api/appointmentTypesApi.js";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -32,6 +34,10 @@ export const store = configureStore({
[appointmentsApi.reducerPath]: appointmentsApi.reducer, [appointmentsApi.reducerPath]: appointmentsApi.reducer,
appointmentsUI: appointmentsReducer, appointmentsUI: appointmentsReducer,
[scheduledAppointmentsApi.reducerPath]: scheduledAppointmentsApi.reducer,
[appointmentTypesApi.reducerPath]: appointmentTypesApi.reducer,
}, },
middleware: (getDefaultMiddleware) => ( middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
@ -42,6 +48,8 @@ export const store = configureStore({
lensTypesApi.middleware, lensTypesApi.middleware,
lensIssuesApi.middleware, lensIssuesApi.middleware,
appointmentsApi.middleware, appointmentsApi.middleware,
scheduledAppointmentsApi.middleware,
appointmentTypesApi.middleware,
) )
), ),
}); });

View File

@ -0,0 +1,8 @@
import PropTypes from "prop-types";
export const DefaultModalPropType = PropTypes.shape({
visible: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
})