med-cms 0.0.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README +161 -0
  2. package/database/config/db-init.js +90 -0
  3. package/database/models/appointments.js +29 -0
  4. package/database/models/clinics.js +22 -0
  5. package/database/models/diseases.js +19 -0
  6. package/database/models/user-addresses.js +22 -0
  7. package/database/models/users.js +36 -0
  8. package/database/services/admin/create-clinic.js +19 -0
  9. package/database/services/admin/db-analytics.js +34 -0
  10. package/database/services/admin/index.js +12 -0
  11. package/database/services/admin/manage-staff.js +41 -0
  12. package/database/services/doctor/conduct-exam.js +33 -0
  13. package/database/services/doctor/get-schedule.js +23 -0
  14. package/database/services/doctor/index.js +10 -0
  15. package/database/services/doctor/patient-access.js +34 -0
  16. package/database/services/patient/book-appointment.js +45 -0
  17. package/database/services/patient/calc-district.js +49 -0
  18. package/database/services/patient/get-profile.js +47 -0
  19. package/database/services/patient/index.js +10 -0
  20. package/index.d.ts +25 -0
  21. package/index.js +79 -0
  22. package/migrate-diseases.js +134 -0
  23. package/package.json +15 -5
  24. package/public/css/admin.css +129 -0
  25. package/public/css/auth.css +55 -0
  26. package/public/css/doctor.css +204 -0
  27. package/public/html/dashboard-admin.html +112 -0
  28. package/public/html/dashboard-doctor.html +78 -0
  29. package/public/html/dashboard-patient.html +113 -0
  30. package/public/html/home.html +0 -0
  31. package/public/html/login.html +48 -0
  32. package/public/js/admin.js +199 -0
  33. package/public/js/auth.js +55 -0
  34. package/public/js/doctor.js +137 -0
  35. package/public/js/patient.js +141 -0
  36. package/server/midleware.js +65 -0
  37. package/server/permissions.js +47 -0
  38. package/server/server.js +264 -0
  39. package/test.js +27 -0
package/README ADDED
@@ -0,0 +1,161 @@
1
+ # MED-CMS (v1.0.0-beta.1) 🚀
2
+
3
+ `med-cms` — это автономный, монолитный медицинский движок (МИС/SaaS) «всё в одном» на базе **Express**, **Sequelize** и **SQLite3**, разработанный для управления сетями клиник, медицинским персоналом, расписанием приемов и электронными картами пациентов.
4
+
5
+ Модуль поставляется со встроенной базой данных, системой разграничения прав доступа на основе **34 медицинских специальностей** и готовым веб-интерфейсом (АРМ Врача, Личный Кабинет Пациента, Панель Суперадминистратора).
6
+
7
+ ---
8
+
9
+ ## 🌟 Ключевые возможности
10
+
11
+ * **📦 Автономность «из коробки»:** База данных SQLite3 не требует установки тяжелых серверов (PostgreSQL/MySQL/MongoDB). Файл БД и таблицы создаются автоматически прямо в папке проекта.
12
+ * **🔐 Безопасность и Сессии:** Авторизация реализована через зашифрованные JWT-токены, хранящиеся в защищенных **HttpOnly Cookie** (жесткая защита от XSS и CSRF атак). JS на клиенте не имеет доступа к токену.
13
+ * **🩺 Матрица прав доступа (34 специальности):** Гибкое разграничение возможностей врачей (Терапевт, Хирург, Кардиохирург, Патологоанатом и др.) через кастомные Middleware. Стоматолог или Эндокринолог не смогут получить доступ к эндпоинтам назначения нейрохирургических операций.
14
+ * **📍 Автоматический расчет участков:** При регистрации или изменении адреса проживания пациента система сама вычисляет номер его терапевтического участка и закрепленный кабинет врача.
15
+ * **📊 Статистика болезней и МКБ-10:** Выделенный независимый справочник заболеваний в базе данных с возможностью автономной миграции и автоматическим инкрементом счетчиков заболеваемости при закрытии приемов врачами.
16
+
17
+ ---
18
+
19
+ ## 📂 Структура проекта
20
+
21
+ Архитектура построена по принципу **Clean Architecture (Чистая архитектура)** с полной изоляцией слоев бэкенда и раздачей статики:
22
+
23
+ ```text
24
+ med-cms/
25
+ ├── package.json # Зависимости движка (express, sequelize, sqlite3, bcryptjs)
26
+ ├── index.js # Главная точка входа (Инициализация и контроль гонки процессов)
27
+ ├── index.d.ts # TypeScript декларации для автокомплита 34 специальностей
28
+ ├── test.js # Скрипт локального запуска тестового сервера
29
+ ├── migrate-diseases.js # Скрипт независимой миграции справочника МКБ-10 в SQLite
30
+ │
31
+ ├── database/
32
+ │ ├── config/
33
+ │ │ └── db-init.js # Инициализация и синхронизация таблиц/связей Sequelize
34
+ │ ├── models/
35
+ │ │ ├── users.js # Таблица пользователей (Числовые автоинкрементные ID)
36
+ │ │ ├── clinics.js # Таблица медицинских учреждений (Больницы, Поликлиники)
37
+ │ │ ├── user-addresses.js # Адреса пациентов с авторасчетом участков и кабинетов
38
+ │ │ ├── diseases.js # Изолированная таблица болезней и статистики
39
+ │ │ └── appointments.js # Журнал приемов (осмотры, жалобы, статусы)
40
+ │ └── services/ # Атомарная бизнес-логика (Разделена по ролям)
41
+ │ ├── admin/ # (create-clinic.js, manage-staff.js, db-analytics.js)
42
+ │ ├── doctor/ # (get-schedule.js, patient-access.js, conduct-exam.js)
43
+ │ └── patient/ # (get-profile.js, calc-district.js, book-appointment.js)
44
+ │
45
+ ├── server/
46
+ │ ├── server.js # Раздельные маршрутизаторы Express (web-страницы и api)
47
+ │ ├── midleware.js # Валидация HttpOnly кук сессий и проверка матрицы разрешений
48
+ │ └── permissions.js # Карта соответствия медицинских специальностей и прав
49
+ │
50
+ └── public/ # Статические файлы фронтенда (HTML, CSS, JS)
51
+ ├── css/ # (auth.css, admin.css, doctor.css)
52
+ ├── js/ # (auth.js, admin.js, doctor.js, patient.js)
53
+ └── html/ # (login.html, dashboard-admin.html, dashboard-doctor.html, ...)
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 🛠️ Установка и быстрая настройка
59
+
60
+ ### 1. Установка модуля
61
+ Установите пакет в ваше Node.js приложение:
62
+ ```bash
63
+ npm install med-cms@1.0.0-beta.1
64
+ ```
65
+
66
+ > **Важно для пользователей Linux:** Пакетам `sequelize` и `sqlite3` требуются компиляторы C++ для сборки бинарных файлов под вашу архитектуру ядра. Перед развертыванием убедитесь, что в системе установлены базовые утилиты сборки:
67
+ > ```bash
68
+ > sudo apt update && sudo apt install -y build-essential python3
69
+ > ```
70
+
71
+ ### 2. Подключение в Express
72
+ Для интеграции CMS в ваш Express-сервер импортируйте функцию инициализации и передайте туда ваш инстанс `app`:
73
+
74
+ ```javascript
75
+ const express = require('express');
76
+ require("dotenv").config();
77
+
78
+ const app = express();
79
+ const PORT = process.env.PORT || 3000;
80
+
81
+ // Импортируем инициализатор нашего npm-модуля
82
+ const initMedCms = require('med-cms');
83
+
84
+ async function startServer() {
85
+ // Запускаем медицинский движок med-cms (он сам настроит БД, роуты и статику)
86
+ await initMedCms(app);
87
+
88
+ app.listen(PORT, () => {
89
+ console.log(`🚀 Основной сервер успешно запущен на http://localhost:${PORT}`);
90
+ });
91
+ }
92
+
93
+ startServer();
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 📊 Миграция расширенного справочника болезней (РБ)
99
+
100
+ Справочник заболеваний полностью изолирован от кода ядра CMS. Для наполнения пустой таблицы `Diseases` актуальными и социально значимыми кодами МКБ-10 для Республики Беларусь, запустите команду миграции перед стартом основного сервера:
101
+
102
+ ```bash
103
+ node node_modules/med-cms/migrate-diseases.js
104
+ # или
105
+ node node_modules\med-cms\migrate-diseases.js
106
+ ```
107
+ Это наполнит отдельную таблицу базы данных 18 распространенными диагнозами (ОРВИ, Острый бронхит, Внебольничная пневмония, Артериальная гипертензия, Остеохондроз, Эндемический зоб и др.), которые автоматически начнут подтягиваться в HTML-тег `<select>` на рабочем месте врача.
108
+
109
+ ---
110
+
111
+ ## 👥 Роли и Маршрутизация (API & Web)
112
+
113
+ Архитектура роутера жестко разделяет отдачу интерфейсов и данных, исключая ошибки парсинга `JSON.parse` в браузере:
114
+
115
+ * **Интерфейс (Web-страницы):** `http://localhost:3000/med/...`
116
+ * **Данные (JSON API):** `http://localhost:3000/med/api/...`
117
+
118
+ ### Матрица маршрутов
119
+
120
+ | Действие | Метод | Маршрут (URL) | Доступ (Роль) |
121
+ | :--- | :--- | :--- | :--- |
122
+ | Страница авторизации и входа | `GET` | `/med/med-login` | Публичный |
123
+ | API Аутентификации (Вход) | `POST` | `/med/api/auth/login` | Публичный |
124
+ | API Завершения сессии (Выход) | `POST` | `/med/api/auth/logout` | Публичный |
125
+ | Личный кабинет Пациента | `GET` | `/med/med-patient` | `patient` |
126
+ | Выгрузка полной медицинской карты | `GET` | `/med/api/patient/me` | `patient` |
127
+ | Бронирование талона к врачу | `POST` | `/med/api/patient/book` | `patient` |
128
+ | АРМ (Рабочее место) Врача | `GET` | `/med/med-doctor` | `doctor` |
129
+ | Загрузка справочника МКБ-10 | `GET` | `/med/api/doctor/diseases-list` | `doctor`, `admin` |
130
+ | Закрытие приема и фиксация осмотра | `POST` | `/med/api/doctor/complete-examination` | `doctor` |
131
+ | Панель Суперадминистратора | `GET` | `/med/med-admin` | `admin` |
132
+ | Создание новой больницы/поликлиники | `POST` | `/med/api/admin/clinics` | `admin` |
133
+ | Регистрация врача (выдача аккаунта) | `POST` | `/med/api/admin/staff` | `admin` |
134
+ | Мониторинг таблиц и аналитика БД | `GET` | `/med/api/admin/stats` | `admin` |
135
+
136
+ ---
137
+
138
+ ## 🛡️ Инструкция к проведению Гипертеста (Тестирование)
139
+
140
+ В модуле подготовлен файл `test.js` для развертывания локальной среды. При самом первом запуске и создании чистой базы данных `db/med-cms.sqlite` скрипт автоматически генерирует мастер-аккаунт **Суперадминистратора** системы:
141
+
142
+ * **Логин (Email):** `admin@medcms.ru`
143
+ * **Пароль:** `SuperAdminPassword2026`
144
+ * **Физический ID в базе:** `1`
145
+
146
+ ### Алгоритм проведения сквозного теста:
147
+ 1. Выполните миграцию справочника МКБ-10: `node migrate-diseases.js`.
148
+ 2. Запустите тестовую среду: `node test.js`.
149
+ 3. Откройте браузер на `http://localhost:3000/med/med-login` и войдите под аккаунтом админа.
150
+ 4. В левой форме добавьте поликлинику. Она запишется в SQLite под числовым `ID: 1`.
151
+ 5. Кликните на карточку «Всего клиник», чтобы развернуть интерактивную таблицу и проверить числовой ID.
152
+ 6. В правой форме зарегистрируйте аккаунт врача, выбрав созданную поликлинику №1 из выпадающего списка.
153
+ 7. Выйдите из админки, нажмите «Зарегистрироваться как пациент» и создайте профиль.
154
+ 8. В кабинете пациента выберите созданного врача, дату, время и нажмите **«Забронировать талон»**. Талон мгновенно отобразится в блоке предстоящих приемов.
155
+ 9. Авторизуйтесь под созданным врачом. Пациент появится в вашей очереди. Нажмите «Принять», заполните жалобы, выберите из подгрузившегося справочника диагноз *«Острый бронхит»* и закройте прием. Системные счетчики зафиксируют визит, а статистика болезни обновится в SQLite!
156
+
157
+ ---
158
+
159
+ ## 📄 Лицензия
160
+
161
+ Проект распространяется на условиях открытой лицензии **MIT**. Разработано для создания масштабируемых и отказоустойчивых медицинских информационных систем. 🇧🇾
@@ -0,0 +1,90 @@
1
+ const { Sequelize, DataTypes } = require('sequelize');
2
+ const path = require('path');
3
+ const logger = require("custom-color-logs").print;
4
+
5
+ let sequelize;
6
+
7
+ function getSequelizeInstance(dbPath) {
8
+ if (!sequelize) {
9
+ const storagePath = dbPath || path.join(process.cwd(), 'med-cms.sqlite');
10
+ sequelize = new Sequelize({
11
+ dialect: 'sqlite',
12
+ storage: storagePath,
13
+ logging: false
14
+ });
15
+ }
16
+ return sequelize;
17
+ }
18
+
19
+ async function initDatabase(dbPath) {
20
+ const instance = getSequelizeInstance(dbPath);
21
+
22
+ try {
23
+ await instance.authenticate();
24
+ console.log(logger.DatabaseMedInfo("Успешное подключение к SQLite через Sequelize."));
25
+
26
+ // 1. Импортируем функции моделей и сразу инициализируем их инстансом sequelize
27
+ const models = {
28
+ Clinic: require(path.join(__dirname, '..', 'models', 'clinics'))(instance, DataTypes),
29
+ User: require(path.join(__dirname, '..', 'models', 'users'))(instance, DataTypes),
30
+ UserAddress: require(path.join(__dirname, '..', 'models', 'user-addresses'))(instance, DataTypes),
31
+ Disease: require(path.join(__dirname, '..', 'models', 'diseases'))(instance, DataTypes),
32
+ Appointment: require(path.join(__dirname, '..', 'models', 'appointments'))(instance, DataTypes)
33
+ };
34
+
35
+ // 2. Автоматически запускаем связи, которые мы прописали внутри файлов таблиц
36
+ Object.keys(models).forEach((modelName) => {
37
+ if (models[modelName].associate) {
38
+ models[modelName].associate(models);
39
+ }
40
+ });
41
+
42
+ await instance.sync({ alter: true });
43
+ console.log(logger.DatabaseMedInfo("Все таблицы базы данных успешно синхронизированы."));
44
+ } catch (error) {
45
+ console.log(logger.DatabaseMedError('Ошибка инициализации базы данных:', error));
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ // В самом конце вашего database/config/db-init.js вместо старого exports:
51
+
52
+ const db = {
53
+ getSequelizeInstance,
54
+ initDatabase,
55
+ get sequelize() { return sequelize; },
56
+ // Динамически прокидываем модели наружу, чтобы они были доступны как db.User, db.Clinic и т.д.
57
+ models: {}
58
+ };
59
+
60
+ // Перепишем функцию initDatabase, чтобы она сохраняла модели в db.models и самом объекте db
61
+ async function initDatabase(dbPath) {
62
+ const instance = getSequelizeInstance(dbPath);
63
+
64
+ try {
65
+ await instance.authenticate();
66
+ console.log(logger.DatabaseMedInfo("Успешное подключение к SQLite через Sequelize."));
67
+
68
+ // Инициализируем и сохраняем в объект
69
+ db.models.Clinic = db.Clinic = require(path.join(__dirname, '..', 'models', 'clinics'))(instance, DataTypes);
70
+ db.models.User = db.User = require(path.join(__dirname, '..', 'models', 'users'))(instance, DataTypes);
71
+ db.models.UserAddress = db.UserAddress = require(path.join(__dirname, '..', 'models', 'user-addresses'))(instance, DataTypes);
72
+ db.models.Disease = db.Disease = require(path.join(__dirname, '..', 'models', 'diseases'))(instance, DataTypes);
73
+ db.models.Appointment = db.Appointment = require(path.join(__dirname, '..', 'models', 'appointments'))(instance, DataTypes);
74
+
75
+ // Запускаем связи
76
+ Object.keys(db.models).forEach((modelName) => {
77
+ if (db.models[modelName].associate) {
78
+ db.models[modelName].associate(db.models);
79
+ }
80
+ });
81
+
82
+ await instance.sync({ alter: true });
83
+ console.log(logger.DatabaseMedInfo("Все таблицы базы данных успешно синхронизированы."));
84
+ } catch (error) {
85
+ console.log(logger.DatabaseMedError('Ошибка инициализации базы данных:', error));
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ module.exports = db;
@@ -0,0 +1,29 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const Appointment = sequelize.define('Appointment', {
3
+ id: {
4
+ type: DataTypes.UUID,
5
+ defaultValue: DataTypes.UUIDV4,
6
+ primaryKey: true
7
+ },
8
+ date: { type: DataTypes.DATEONLY, allowNull: false },
9
+ time: { type: DataTypes.STRING, allowNull: false },
10
+ status: {
11
+ type: DataTypes.ENUM('ожидается', 'завершен', 'отменен'),
12
+ defaultValue: 'ожидается',
13
+ allowNull: false
14
+ },
15
+ complaints: { type: DataTypes.TEXT, allowNull: true },
16
+ objectively: { type: DataTypes.TEXT, allowNull: true },
17
+ doctorNotes: { type: DataTypes.TEXT, allowNull: true }
18
+ }, {
19
+ timestamps: true
20
+ });
21
+
22
+ Appointment.associate = (models) => {
23
+ Appointment.belongsTo(models.User, { as: 'Patient', foreignKey: 'PatientId', onDelete: 'CASCADE' });
24
+ Appointment.belongsTo(models.User, { as: 'Doctor', foreignKey: 'DoctorId', onDelete: 'CASCADE' });
25
+ Appointment.belongsTo(models.Disease, { foreignKey: 'DiseaseId', onDelete: 'SET NULL' });
26
+ };
27
+
28
+ return Appointment;
29
+ };
@@ -0,0 +1,22 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const Clinic = sequelize.define('Clinic', {
3
+ id: {
4
+ type: DataTypes.INTEGER,
5
+ autoIncrement: true,
6
+ primaryKey: true
7
+ },
8
+ name: { type: DataTypes.STRING, allowNull: false, unique: true },
9
+ type: { type: DataTypes.ENUM('больница', 'поликлиника', 'станция скорой помощи', 'частный медицинский центр'), allowNull: false },
10
+ address: { type: DataTypes.STRING, allowNull: false },
11
+ phone: { type: DataTypes.STRING, allowNull: true }
12
+ }, {
13
+ timestamps: true
14
+ });
15
+
16
+ // Настройка связи прямо в таблице клиник
17
+ Clinic.associate = (models) => {
18
+ Clinic.hasMany(models.User, { foreignKey: 'ClinicId' });
19
+ };
20
+
21
+ return Clinic;
22
+ };
@@ -0,0 +1,19 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const Disease = sequelize.define('Disease', {
3
+ id: {
4
+ type: DataTypes.INTEGER,
5
+ autoIncrement: true,
6
+ primaryKey: true
7
+ },
8
+ name: { type: DataTypes.STRING, allowNull: false, unique: true },
9
+ code: { type: DataTypes.STRING, allowNull: false, unique: true },
10
+ treatmentRecommendations: { type: DataTypes.TEXT, allowNull: true },
11
+ recommendedMedications: { type: DataTypes.TEXT, allowNull: true },
12
+ casesCount: { type: DataTypes.INTEGER, defaultValue: 0, allowNull: false }
13
+ }, {
14
+ timestamps: true
15
+ });
16
+
17
+ // У этой модели нет прямых исходящих связей (зависит от Appointment, но связь прописана там)
18
+ return Disease;
19
+ };
@@ -0,0 +1,22 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const UserAddress = sequelize.define('UserAddress', {
3
+ id: {
4
+ type: DataTypes.UUID,
5
+ defaultValue: DataTypes.UUIDV4,
6
+ primaryKey: true
7
+ },
8
+ city: { type: DataTypes.STRING, allowNull: false },
9
+ street: { type: DataTypes.STRING, allowNull: false },
10
+ houseNumber: { type: DataTypes.STRING, allowNull: false },
11
+ assignedDistrict: { type: DataTypes.STRING, allowNull: true },
12
+ assignedCabinet: { type: DataTypes.STRING, allowNull: true }
13
+ }, {
14
+ timestamps: true
15
+ });
16
+
17
+ UserAddress.associate = (models) => {
18
+ UserAddress.belongsTo(models.User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
19
+ };
20
+
21
+ return UserAddress;
22
+ };
@@ -0,0 +1,36 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const User = sequelize.define('User', {
3
+ id: {
4
+ type: DataTypes.INTEGER,
5
+ autoIncrement: true,
6
+ primaryKey: true
7
+ },
8
+ email: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { isEmail: true } },
9
+ password: { type: DataTypes.STRING, allowNull: false },
10
+ fullName: { type: DataTypes.STRING, allowNull: false },
11
+ role: { type: DataTypes.ENUM('patient', 'doctor', 'admin'), defaultValue: 'patient', allowNull: false },
12
+ specialty: {
13
+ type: DataTypes.ENUM(
14
+ 'Терапевт', 'Педиатр', 'Врач общей практики', 'Кардиолог', 'Невролог',
15
+ 'Эндокринолог', 'Гастроэнтеролог', 'Пульмонолог', 'Нефролог', 'Ревматолог',
16
+ 'Гематолог', 'Хирург', 'Травматолог-ортопед', 'Сосудистый хирург', 'Нейрохирург',
17
+ 'Кардиохирург', 'Пластический хирург', 'Офтальмолог', 'Отоларинголог', 'Дерматолог',
18
+ 'Стоматолог', 'Гинеколог', 'Уролог', 'Онколог', 'Инфекционист', 'Аллерголог-иммунолог',
19
+ 'Психиатр', 'Психотерапевт', 'Нарколог', 'Анестезиолог-реаниматолог', 'Рентгенолог',
20
+ 'Врач УЗД', 'Эндоскопист', 'Патологоанатом'
21
+ ),
22
+ allowNull: true
23
+ },
24
+ isTemporaryPassword: { type: DataTypes.BOOLEAN, defaultValue: false }
25
+ }, {
26
+ timestamps: true
27
+ });
28
+
29
+ // Настройка связей пользователя
30
+ User.associate = (models) => {
31
+ User.belongsTo(models.Clinic, { foreignKey: 'ClinicId', onDelete: 'SET NULL' });
32
+ User.hasOne(models.UserAddress, { foreignKey: 'UserId' });
33
+ };
34
+
35
+ return User;
36
+ };
@@ -0,0 +1,19 @@
1
+ const path = require("path");
2
+ const Clinic = require(path.join(__dirname, '..', '..', 'models', 'clinics'));
3
+ const createClinic = {
4
+ async create(clinicData) {
5
+ const { name, type, address, phone } = clinicData;
6
+ if (!name || !type || !address) {
7
+ throw new Error('Поля name, type и address обязательны для заполнения');
8
+ }
9
+ const candidate = await Clinic.findOne({ where: { name } });
10
+ if (candidate) {
11
+ throw new Error('Медицинское учреждение с таким названием уже зарегистрировано');
12
+ }
13
+ return await Clinic.create({ name, type, address, phone });
14
+ },
15
+ async getAll() {
16
+ return await Clinic.findAll({ order: [['name', 'ASC']] });
17
+ }
18
+ };
19
+ module.exports = createClinic;
@@ -0,0 +1,34 @@
1
+ const path = require("path")
2
+ const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
3
+ const Clinic = require(path.join(__dirname, '..', '..', 'models', 'clinics'));
4
+ const Disease = require(path.join(__dirname, '..', '..', 'models', 'diseases'));
5
+ const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
6
+ const dbAnalytics = {
7
+ async getStats() {
8
+ const totalClinics = await Clinic.count();
9
+ const totalPatients = await User.count({ where: { role: 'patient' } });
10
+ const totalStaff = await User.count({ where: { role: 'doctor' } });
11
+ const totalAppointments = await Appointment.count();
12
+ const topDiseases = await Disease.findAll({
13
+ limit: 5,
14
+ order: [['casesCount', 'DESC']],
15
+ attributes: ['name', 'code', 'casesCount']
16
+ });
17
+ const staffBySpecialty = await User.findAll({
18
+ where: { role: 'doctor' },
19
+ attributes: ['specialty', [User.sequelize.fn('COUNT', User.sequelize.col('id')), 'count']],
20
+ group: ['specialty']
21
+ });
22
+ return {
23
+ summary: {
24
+ totalClinics,
25
+ totalPatients,
26
+ totalStaff,
27
+ totalAppointments
28
+ },
29
+ topDiseases,
30
+ staffBySpecialty
31
+ };
32
+ }
33
+ };
34
+ module.exports = dbAnalytics;
@@ -0,0 +1,12 @@
1
+ const path = require("path")
2
+ const createClinic = require(path.join(__dirname, 'create-clinic'));
3
+ const manageStaff = require(path.join(__dirname, 'manage-staff'));
4
+ const dbAnalytics = require(path.join(__dirname, 'db-analytics'));
5
+ const AdminService = {
6
+ createClinic: createClinic.create,
7
+ getAllClinics: createClinic.getAll,
8
+ createStaffAccount: manageStaff.createAccount,
9
+ deleteAccount: manageStaff.deleteAccount,
10
+ getSystemStats: dbAnalytics.getStats
11
+ };
12
+ module.exports = AdminService;
@@ -0,0 +1,41 @@
1
+ const path = require("path")
2
+ const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
3
+ const Clinic = require(path.join(__dirname, '..', '..', 'models', 'clinics'));
4
+ const bcrypt = require('bcrypt');
5
+ const manageStaff = {
6
+ async createAccount(staffData) {
7
+ const { email, rawPassword, fullName, medicalProfile } = staffData;
8
+ const ClinicId = staffData.ClinicId || staffData.clinicId;
9
+ if (!email || !rawPassword || !fullName || !medicalProfile || !ClinicId) {
10
+ throw new Error('Необходимо заполнить обязательные поля: email, rawPassword, fullName, medicalProfile, clinicId');
11
+ }
12
+ const clinicExists = await Clinic.findByPk(ClinicId);
13
+ if (!clinicExists) {
14
+ throw new Error(`Медицинское учреждение с ID №${ClinicId} не найдено.`);
15
+ }
16
+ const candidate = await User.findOne({ where: { email } });
17
+ if (candidate) {
18
+ throw new Error('Сотрудник с таким email уже зарегистрирован');
19
+ }
20
+ let finalPosition = 'Врач';
21
+ let finalSpecialty = medicalProfile;
22
+ const purePositions = ['Главврач', 'Медсестра', 'Врач скорой помощи', 'Фельдшер', 'Регистратор'];
23
+ if (purePositions.includes(medicalProfile)) {
24
+ finalPosition = medicalProfile;
25
+ finalSpecialty = 'Общий медицинский профиль';
26
+ }
27
+ const hashedPassword = await bcrypt.hash(rawPassword, 10);
28
+ const newStaff = await User.create({
29
+ email,
30
+ password: hashedPassword,
31
+ fullName,
32
+ role: 'doctor',
33
+ position: finalPosition,
34
+ specialty: finalSpecialty,
35
+ ClinicId: Number(ClinicId),
36
+ isTemporaryPassword: true
37
+ });
38
+ return newStaff;
39
+ }
40
+ };
41
+ module.exports = manageStaff;
@@ -0,0 +1,33 @@
1
+ const path = require("path")
2
+ const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
3
+ const Disease = require(path.join(__dirname, '..', '..', 'models', 'diseases'));
4
+ const conductExam = {
5
+ async saveResult(examData) {
6
+ const { appointmentId, complaints, objectively, doctorNotes, diseaseId } = examData;
7
+ if (!appointmentId) {
8
+ throw new Error('ID приема обязателен для сохранения результатов');
9
+ }
10
+ const appointment = await Appointment.findByPk(appointmentId);
11
+ if (!appointment) {
12
+ throw new Error('Указанный прием не найден');
13
+ }
14
+ if (appointment.status === 'завершен') {
15
+ throw new Error('Этот прием уже был успешно завершен ранее');
16
+ }
17
+ appointment.complaints = complaints;
18
+ appointment.objectively = objectively;
19
+ appointment.doctorNotes = doctorNotes;
20
+ appointment.status = 'завершен';
21
+ if (diseaseId) {
22
+ const disease = await Disease.findByPk(diseaseId);
23
+ if (!disease) {
24
+ throw new Error('Указанная болезнь не найдена в справочнике диагнозов');
25
+ }
26
+ appointment.DiseaseId = diseaseId;
27
+ await disease.increment('casesCount', { by: 1 });
28
+ }
29
+ await appointment.save();
30
+ return appointment;
31
+ }
32
+ };
33
+ module.exports = conductExam;
@@ -0,0 +1,23 @@
1
+ const path = require("path")
2
+ const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
3
+ const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
4
+ const getSchedule = {
5
+ async byDoctorId(doctorId) {
6
+ if (!doctorId) {
7
+ throw new Error('ID врача не указан');
8
+ }
9
+ return await Appointment.findAll({
10
+ where: {
11
+ DoctorId: doctorId,
12
+ status: 'ожидается'
13
+ },
14
+ include: [{
15
+ model: User,
16
+ as: 'Patient',
17
+ attributes: ['id', 'fullName', 'email']
18
+ }],
19
+ order: [['date', 'ASC'], ['time', 'ASC']]
20
+ });
21
+ }
22
+ };
23
+ module.exports = getSchedule;
@@ -0,0 +1,10 @@
1
+ const path = require("path")
2
+ const getSchedule = require(path.join(__dirname, 'get-schedule'));
3
+ const patientAccess = require(path.join(__dirname, 'patient-access'));
4
+ const conductExam = require(path.join(__dirname, 'conduct-exam'));
5
+ const DoctorService = {
6
+ getMySchedule: getSchedule.byDoctorId,
7
+ getPatientHistory: patientAccess.getHistory,
8
+ saveExamination: conductExam.saveResult
9
+ };
10
+ module.exports = DoctorService;
@@ -0,0 +1,34 @@
1
+ const path = require("path");
2
+ const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
3
+ const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
4
+ const Disease = require(path.join(__dirname, '..', '..', 'models', 'diseases'));
5
+ const UserAddress = require(path.join(__dirname, '..', '..', 'models', 'user-addresses'));
6
+ const patientAccess = {
7
+ async getHistory(patientId) {
8
+ if (!patientId) {
9
+ throw new Error('ID пациента не указан');
10
+ }
11
+ const patient = await User.findByPk(patientId, {
12
+ attributes: ['id', 'fullName', 'email'],
13
+ include: [
14
+ {
15
+ model: UserAddress,
16
+ attributes: ['city', 'street', 'houseNumber', 'assignedDistrict', 'assignedCabinet']
17
+ },
18
+ {
19
+ model: Appointment,
20
+ as: 'PatientAppointments',
21
+ where: { status: 'завершен' },
22
+ include: [{ model: Disease, attributes: ['name', 'code'] }],
23
+ required: false
24
+ }
25
+ ],
26
+ order: [[{ model: Appointment, as: 'PatientAppointments' }, 'date', 'DESC']]
27
+ });
28
+ if (!patient) {
29
+ throw new Error('Пациент не найден в базе данных');
30
+ }
31
+ return patient;
32
+ }
33
+ };
34
+ module.exports = patientAccess;
@@ -0,0 +1,45 @@
1
+ const path = require("path")
2
+ const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
3
+ const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
4
+ const bookAppointment = {
5
+ async create(patientId, appointmentData) {
6
+ const { doctorId, date, time } = appointmentData;
7
+ if (!doctorId || !date || !time) {
8
+ throw new Error('Необходимо выбрать врача, дату и время приема');
9
+ }
10
+ const doctor = await User.findOne({ where: { id: doctorId, role: 'doctor' } });
11
+ if (!doctor) {
12
+ throw new Error('Указанный врач не найден в системе или не имеет прав на прием');
13
+ }
14
+ const isSlotBusy = await Appointment.findOne({
15
+ where: {
16
+ DoctorId: doctorId,
17
+ date: date,
18
+ time: time,
19
+ status: 'ожидается'
20
+ }
21
+ });
22
+ if (isSlotBusy) {
23
+ throw new Error(`Время ${time} на дату ${date} к врачу ${doctor.fullName} уже занято. Выберите другой слот.`);
24
+ }
25
+ const isPatientDoubleBooked = await Appointment.findOne({
26
+ where: {
27
+ PatientId: patientId,
28
+ DoctorId: doctorId,
29
+ date: date,
30
+ status: 'ожидается'
31
+ }
32
+ });
33
+ if (isPatientDoubleBooked) {
34
+ throw new Error(`Вы уже записаны к врачу ${doctor.fullName} на дату ${date}. Нельзя бронировать несколько талонов на один день.`);
35
+ }
36
+ return await Appointment.create({
37
+ date,
38
+ time,
39
+ status: 'ожидается',
40
+ PatientId: patientId,
41
+ DoctorId: doctorId
42
+ });
43
+ }
44
+ };
45
+ module.exports = bookAppointment;