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.
- package/README +161 -0
- package/database/config/db-init.js +90 -0
- package/database/models/appointments.js +29 -0
- package/database/models/clinics.js +22 -0
- package/database/models/diseases.js +19 -0
- package/database/models/user-addresses.js +22 -0
- package/database/models/users.js +36 -0
- package/database/services/admin/create-clinic.js +19 -0
- package/database/services/admin/db-analytics.js +34 -0
- package/database/services/admin/index.js +12 -0
- package/database/services/admin/manage-staff.js +41 -0
- package/database/services/doctor/conduct-exam.js +33 -0
- package/database/services/doctor/get-schedule.js +23 -0
- package/database/services/doctor/index.js +10 -0
- package/database/services/doctor/patient-access.js +34 -0
- package/database/services/patient/book-appointment.js +45 -0
- package/database/services/patient/calc-district.js +49 -0
- package/database/services/patient/get-profile.js +47 -0
- package/database/services/patient/index.js +10 -0
- package/index.d.ts +25 -0
- package/index.js +79 -0
- package/migrate-diseases.js +134 -0
- package/package.json +15 -5
- package/public/css/admin.css +129 -0
- package/public/css/auth.css +55 -0
- package/public/css/doctor.css +204 -0
- package/public/html/dashboard-admin.html +112 -0
- package/public/html/dashboard-doctor.html +78 -0
- package/public/html/dashboard-patient.html +113 -0
- package/public/html/home.html +0 -0
- package/public/html/login.html +48 -0
- package/public/js/admin.js +199 -0
- package/public/js/auth.js +55 -0
- package/public/js/doctor.js +137 -0
- package/public/js/patient.js +141 -0
- package/server/midleware.js +65 -0
- package/server/permissions.js +47 -0
- package/server/server.js +264 -0
- package/test.js +27 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const UserAddress = require(path.join(__dirname, '..', '..', 'models', 'user-addresses'));
|
|
3
|
+
const calcDistrict = {
|
|
4
|
+
determineLocation(street, houseNumber) {
|
|
5
|
+
const cleanedStreet = street.trim().toLowerCase();
|
|
6
|
+
if (cleanedStreet.includes('ленина') || cleanedStreet.includes('центральная')) {
|
|
7
|
+
return {
|
|
8
|
+
assignedDistrict: 'Участок №1 (Центральный)',
|
|
9
|
+
assignedCabinet: 'Кабинет №102 (Терапевт / Педиатр)'
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (cleanedStreet.includes('пушкина') || cleanedStreet.includes('мичурина')) {
|
|
13
|
+
return {
|
|
14
|
+
assignedDistrict: 'Участок №2 (Северный)',
|
|
15
|
+
assignedCabinet: 'Кабинет №205 (Терапевт / Педиатр)'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
assignedDistrict: 'Участок №3 (Общий)',
|
|
20
|
+
assignedCabinet: 'Кабинет №301 (Врач общей практики)'
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
async saveAddress(userId, addressData) {
|
|
24
|
+
const { city, street, houseNumber } = addressData;
|
|
25
|
+
if (!city || !street || !houseNumber) {
|
|
26
|
+
throw new Error('Для привязки адреса необходимы город, улица и номер дома');
|
|
27
|
+
}
|
|
28
|
+
const { assignedDistrict, assignedCabinet } = this.determineLocation(street, houseNumber);
|
|
29
|
+
const existingAddress = await UserAddress.findOne({ where: { UserId: userId } });
|
|
30
|
+
if (existingAddress) {
|
|
31
|
+
return await existingAddress.update({
|
|
32
|
+
city,
|
|
33
|
+
street,
|
|
34
|
+
houseNumber,
|
|
35
|
+
assignedDistrict,
|
|
36
|
+
assignedCabinet
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return await UserAddress.create({
|
|
40
|
+
UserId: userId,
|
|
41
|
+
city,
|
|
42
|
+
street,
|
|
43
|
+
houseNumber,
|
|
44
|
+
assignedDistrict,
|
|
45
|
+
assignedCabinet
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
module.exports = calcDistrict;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const path = require("path")
|
|
2
|
+
const User = require(path.join(__dirname, '..', '..', 'models', 'users'));
|
|
3
|
+
const UserAddress = require(path.join(__dirname, '..', '..', 'models', 'user-addresses'));
|
|
4
|
+
const Appointment = require(path.join(__dirname, '..', '..', 'models', 'appointments'));
|
|
5
|
+
const Disease = require(path.join(__dirname, '..', '..', 'models', 'diseases'));
|
|
6
|
+
const getProfile = {
|
|
7
|
+
async getFullData(patientId) {
|
|
8
|
+
if (!patientId) {
|
|
9
|
+
throw new Error('ID пациента не указан');
|
|
10
|
+
}
|
|
11
|
+
const profile = await User.findByPk(patientId, {
|
|
12
|
+
attributes: { exclude: ['password'] },
|
|
13
|
+
include: [
|
|
14
|
+
{
|
|
15
|
+
model: UserAddress,
|
|
16
|
+
attributes: ['city', 'street', 'houseNumber', 'assignedDistrict', 'assignedCabinet']
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
if (!profile) {
|
|
21
|
+
throw new Error('Профиль пациента не найден в базе данных');
|
|
22
|
+
}
|
|
23
|
+
const appointments = await Appointment.findAll({
|
|
24
|
+
where: { PatientId: patientId },
|
|
25
|
+
include: [
|
|
26
|
+
{
|
|
27
|
+
model: User,
|
|
28
|
+
as: 'Doctor',
|
|
29
|
+
attributes: ['fullName', 'specialty']
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
model: Disease,
|
|
33
|
+
attributes: ['name', 'code', 'treatmentRecommendations', 'recommendedMedications']
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
order: [
|
|
37
|
+
['date', 'DESC'],
|
|
38
|
+
['time', 'DESC']
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
personalInfo: profile,
|
|
43
|
+
medicalHistory: appointments
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
module.exports = getProfile;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const calcDistrict = require(path.join(__dirname, 'calc-district'));
|
|
3
|
+
const getProfile = require(path.join(__dirname, 'get-profile'));
|
|
4
|
+
const bookAppointment = require(path.join(__dirname, 'book-appointment'));
|
|
5
|
+
const PatientService = {
|
|
6
|
+
savePatientAddress: calcDistrict.saveAddress.bind(calcDistrict),
|
|
7
|
+
getFullMyData: getProfile.getFullData,
|
|
8
|
+
bookAppointment: bookAppointment.create.bind(bookAppointment)
|
|
9
|
+
};
|
|
10
|
+
module.exports = PatientService;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
export interface MedCmsConfig {
|
|
4
|
+
jwtSecret: string;
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DoctorSpecialty =
|
|
9
|
+
|
|
10
|
+
| 'Терапевт' | 'Педиатр' | 'Врач общей практики' | 'Кардиолог' | 'Невролог'
|
|
11
|
+
| 'Эндокринолог' | 'Гастроэнтеролог' | 'Пульмонолог' | 'Нефролог' | 'Ревматолог'
|
|
12
|
+
| 'Гематолог' | 'Хирург' | 'Травматолог-ортопед' | 'Сосудистый хирург' | 'Нейрохирург'
|
|
13
|
+
| 'Кардиохирург' | 'Пластический хирург' | 'Офтальмолог' | 'Отоларинголог' | 'Дерматолог'
|
|
14
|
+
| 'Стоматолог' | 'Гинеколог' | 'Уролог' | 'Онколог' | 'Инфекционист' | 'Аллерголог-иммунолог'
|
|
15
|
+
| 'Психиатр' | 'Психотерапевт' | 'Нарколог' | 'Анестезиолог-реаниматолог' | 'Рентгенолог'
|
|
16
|
+
| 'Врач УЗД' | 'Эндоскопист' | 'Патологоанатом';
|
|
17
|
+
|
|
18
|
+
export type UserRole = 'patient' | 'doctor' | 'admin';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Инициализирует модуль med-cms и возвращает Express Router
|
|
22
|
+
*/
|
|
23
|
+
declare function initMedCms(config: MedCmsConfig): Router;
|
|
24
|
+
|
|
25
|
+
export default initMedCms;
|
package/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require("dotenv").config({debug:false});
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const cookieParser = require('cookie-parser');
|
|
4
|
+
const chalk = require("chalk-palette");
|
|
5
|
+
const logger = require("custom-color-logs").print;
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
// Оставляем только импорт самого модуля базы данных
|
|
10
|
+
const dbConfig = require(path.join(__dirname, "database", "config", "db-init.js"));
|
|
11
|
+
const server = require('./server/server');
|
|
12
|
+
const bcrypt = require('bcrypt');
|
|
13
|
+
|
|
14
|
+
async function init(app) {
|
|
15
|
+
await chalk.animation.textShimmer("MED-CMS");
|
|
16
|
+
console.log(logger.ServerMedInfo("Запуск сервера..."));
|
|
17
|
+
await chalk.animation.retroLines({duration:4000, linesCount:4, offset:3});
|
|
18
|
+
|
|
19
|
+
if (app && typeof app === 'function' && typeof app.handle === 'function') {
|
|
20
|
+
console.log(logger.ServerMedInfo("Проверка на app пройдена успешно!"));
|
|
21
|
+
} else {
|
|
22
|
+
console.log(logger.ServerMedError("app не передан или повреждён!"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const dbPath = path.join(__dirname, "db", "med-cms.sqlite");
|
|
28
|
+
const dbDir = path.dirname(dbPath);
|
|
29
|
+
if (!fs.existsSync(dbDir)){
|
|
30
|
+
fs.mkdirSync(dbDir, { extended: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Инициализируем БД
|
|
34
|
+
await dbConfig.initDatabase(dbPath);
|
|
35
|
+
console.log(logger.ServerMedInfo("База данных успешно инициализирована"));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.log(logger.DatabaseError("Критическая ошибка запуска базы данных med-cms:", err));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// ИСПРАВЛЕНИЕ: Достаем User из проинициализированного объекта базы данных ОНЛАЙН
|
|
43
|
+
const User = dbConfig.User;
|
|
44
|
+
|
|
45
|
+
const adminExists = await User.findOne({ where: { role: 'admin' } });
|
|
46
|
+
if (!adminExists) {
|
|
47
|
+
console.log(logger.ServerMedInfo("Администратор не найден. Создаю аккаунт по умолчанию..."));
|
|
48
|
+
const adminPassword = "SuperAdminPassword2026";
|
|
49
|
+
const hashedPassword = await bcrypt.hash(adminPassword, 10);
|
|
50
|
+
await User.create({
|
|
51
|
+
email: "admin@medcms.ru",
|
|
52
|
+
password: hashedPassword,
|
|
53
|
+
fullName: "Главный Администратор",
|
|
54
|
+
role: "admin",
|
|
55
|
+
specialty: null
|
|
56
|
+
});
|
|
57
|
+
console.log(logger.ServerMedInfo(`Аккаунт суперадмина успешно создан in SQLite!`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(logger.ServerMedInfo("Администратор уже существует в базе данных."));
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.log(logger.ServerMedError("Ошибка при автоматическом создании админа: " + error.message));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(logger.ServerMedInfo("Подключение роутов..."));
|
|
66
|
+
const rout = express.Router();
|
|
67
|
+
const med = server(process.env.JWT_SECRET);
|
|
68
|
+
const r_api = med.api;
|
|
69
|
+
const r_web = med.web;
|
|
70
|
+
|
|
71
|
+
rout.use(cookieParser());
|
|
72
|
+
rout.use(express.json());
|
|
73
|
+
rout.use(express.urlencoded({ extended: true }));
|
|
74
|
+
rout.use("/med", r_web);
|
|
75
|
+
rout.use("/med/api", r_api);
|
|
76
|
+
app.use("/", rout);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = init;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const logger = require("custom-color-logs");
|
|
2
|
+
const chalk = require("chalk-palette");
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { initDatabase } = require(path.join(__dirname, 'database', 'config', 'db-init'));
|
|
5
|
+
const Disease = require(path.join(__dirname, 'database', 'models', 'diseases'));
|
|
6
|
+
const externalDiseasesList = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Острая респираторная вирусная инфекция (ОРВИ) верхних дыхательных путей',
|
|
9
|
+
code: 'J06.9',
|
|
10
|
+
treatmentRecommendations: 'Домашний режим, обильное теплое питье, проветривание помещения, промывание носа изотоническими растворами.',
|
|
11
|
+
recommendedMedications: 'Ибупрофен (при лихорадке), Ксилометазолин (капли в нос), Витамин C'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'Острый бронхит',
|
|
15
|
+
code: 'J20.9',
|
|
16
|
+
treatmentRecommendations: 'Постельный режим, ингаляции при отсутствии лихорадки, отказ от курения, теплое щелочное питье.',
|
|
17
|
+
recommendedMedications: 'Амброксол, Ацетилцистеин (АЦЦ), Парацетамол'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Внебольничная пневмония бактериальная',
|
|
21
|
+
code: 'J15.9',
|
|
22
|
+
treatmentRecommendations: 'Строгий постельный режим, контроль сатурации, повторный осмотр врача через 48 часов, при тяжелом течении — госпитализация.',
|
|
23
|
+
recommendedMedications: 'Амоксициллин + Клавулановая кислота, Азитромицин, Левофлоксацин'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Острый тонзиллит (Ангина стрептококковая)',
|
|
27
|
+
code: 'J03.0',
|
|
28
|
+
treatmentRecommendations: 'Щадящая диета (жидкая и теплая пища), полоскание зева антисептиками, строгий постельный режим во избежание осложнений на сердце и почки.',
|
|
29
|
+
recommendedMedications: 'Феноксиметилпенициллин, Цефуроксим, Хлоргексидин (спрей)'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Артериальная гипертензия (Гипертоническая болезнь II стадии)',
|
|
33
|
+
code: 'I11.9',
|
|
34
|
+
treatmentRecommendations: 'Ограничение поваренной соли до 5 г/сутки, снижение веса, отказ от алкоголя и курения, ежедневный пожизненный прием препаратов под контролем АД.',
|
|
35
|
+
recommendedMedications: 'Лизиноприл, Амлодипин, Бисопролол, Индапамид'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Стабильная стенокардия напряжения (ИБС), II функциональный класс',
|
|
39
|
+
code: 'I20.8',
|
|
40
|
+
treatmentRecommendations: 'Ограничение интенсивных физических нагрузок, диета с низким содержанием животных жиров, контроль уровня холестерина.',
|
|
41
|
+
recommendedMedications: 'Ацетилсалициловая кислота (Аспирин-Кардио), Аторвастатин, Нитроглицерин (при приступе)'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Хроническая ишемическая болезнь сердца (Атеросклеротический кардиосклероз)',
|
|
45
|
+
code: 'I25.1',
|
|
46
|
+
treatmentRecommendations: 'Умеренная физическая активность (ходьба), контроль веса, терапия сопутствующей гипертензии.',
|
|
47
|
+
recommendedMedications: 'Периндоприл, Розувастатин, Клопидогрел'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Остеохондроз позвоночника (Радикулопатия, люмбалгия)',
|
|
51
|
+
code: 'M42.1',
|
|
52
|
+
treatmentRecommendations: 'Покой на жестком матрасе в острый период, ограничение наклонов и подъема тяжестей. После стихания болей — ЛФК и плавание.',
|
|
53
|
+
recommendedMedications: 'Диклофенак (инъекции/гель), Мидокалм (толперизон), Комбилипен'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Первичный гонартроз двусторонний (Артроз коленных суставов)',
|
|
57
|
+
code: 'M17.0',
|
|
58
|
+
treatmentRecommendations: 'Снижение нагрузки на суставы (снижение веса), использование трости при ходьбе, лечебная гимнастика для укрепления мышц бедра.',
|
|
59
|
+
recommendedMedications: 'Мелоксикам, Хондроитина сульфат, Нимесулид гель'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Хронический гастрит, обострение. Ассоциированный с Helicobacter pylori',
|
|
63
|
+
code: 'K29.5',
|
|
64
|
+
treatmentRecommendations: 'Диета №1 по Певзнеру (исключить острое, жареное, копченое, газировку). Дробное питание 5-6 раз в день.',
|
|
65
|
+
recommendedMedications: 'Омепразол (или Эзомепразол), Кларитромицин, Амоксициллин'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Язвенная болезнь двенадцатиперстной кишки, стадия обострения',
|
|
69
|
+
code: 'K26.4',
|
|
70
|
+
treatmentRecommendations: 'Строгий отказ от курения и алкоголя, постельный или щадящий режим, контроль стула (исключить скрытое кровотечение).',
|
|
71
|
+
recommendedMedications: 'Рабепразол, Де-Нол (висмута трикалия дицитрат), Фамотидин'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Хронический панкреатит в стадии неполной ремиссии',
|
|
75
|
+
code: 'K86.1',
|
|
76
|
+
treatmentRecommendations: 'Диета №5п (полное исключение жирной, острой пищи и алкоголя). Дробный прием пищи.',
|
|
77
|
+
recommendedMedications: 'Панкреатин (Креон/Мезим), Спазмалгон'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Сахарный диабет 2-го типа без осложнений',
|
|
81
|
+
code: 'E11.9',
|
|
82
|
+
treatmentRecommendations: 'Низкоуглеводная диета (Стол №9), строгое исключение сахара, регулярный контроль глюкозы крови натощак и через 2 часа после еды.',
|
|
83
|
+
recommendedMedications: 'Метформин, Гликлазид'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Эндемический зоб, связанный с йодной недостаточностью (Очень актуально для РБ)',
|
|
87
|
+
code: 'E01.0',
|
|
88
|
+
treatmentRecommendations: 'Использование в пищу исключительно йодированной соли, употребление морепродуктов, контроль уровня ТТГ раз в год.',
|
|
89
|
+
recommendedMedications: 'Калия йодид (Йодомарин)'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Острый аппендицит (Подозрение, направление на госпитализацию)',
|
|
93
|
+
code: 'K35.8',
|
|
94
|
+
treatmentRecommendations: 'Экстренная госпитализация скорой помощью в хирургический стационар. Категорически запрещено прикладывать грелку к животу и принимать обезболивающие до осмотра хирурга.',
|
|
95
|
+
recommendedMedications: 'Медикаментозное лечение противопоказано. Показана экстренная операция.'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'Почечная колика (Мочекаменная болезнь)',
|
|
99
|
+
code: 'N20.1',
|
|
100
|
+
treatmentRecommendations: 'Теплые ванны (при отсутствии макрогематурии и лихорадки), обильное питье при стихании боли для отхождения конкремента.',
|
|
101
|
+
recommendedMedications: 'Дротаверин (Но-Шпа) внутримышечно, Кеторолак, Тамсулозин'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'Острый цистит бактериальный',
|
|
105
|
+
code: 'N30.0',
|
|
106
|
+
treatmentRecommendations: 'Обильное питье (до 2.5 литров в сутки, предпочтительно клюквенный морс), сухое тепло на низ живота, исключение половой жизни на время лечения.',
|
|
107
|
+
recommendedMedications: 'Фосфомицин (Монурал), Канефрон Н, Фуразидин (Фурамаг)'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Соматоформная дисфункция вегетативной нервной системы (НЦД / ВСД)',
|
|
111
|
+
code: 'F45.3',
|
|
112
|
+
treatmentRecommendations: 'Нормализация режима сна и бодрствования, контрастный душ, регулярные кардионагрузки (бег, велосипед), психотерапия.',
|
|
113
|
+
recommendedMedications: 'Адаптол (Мебикар), Магний B6, пустырника экстракт'
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
async function runMigration() {
|
|
117
|
+
try {
|
|
118
|
+
console.log(logger.DatabaseMedMigrationInfo('Подключение к базе данных для импорта справочника...'));
|
|
119
|
+
await initDatabase(path.join(__dirname, "db", "med-cms.sqlite"));
|
|
120
|
+
console.log(logger.DatabaseMedMigrationInfo('Проверка текущего состояния таблицы Diseases...'));
|
|
121
|
+
const count = await Disease.count();
|
|
122
|
+
if (count === 0) {
|
|
123
|
+
await Disease.bulkCreate(externalDiseasesList);
|
|
124
|
+
console.log(logger.DatabaseMedMigrationInfo(`УСПЕШНО! В отдельную таблицу Diseases импортировано ${externalDiseasesList.length} диагнозов.`));
|
|
125
|
+
} else {
|
|
126
|
+
console.log(logger.DatabaseMedMigrationInfo(`Таблица Diseases уже содержит записи (${count} шт.). Импорт не требуется.`));
|
|
127
|
+
}
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.log(logger.DatabaseMedMigrationError('Критическая ошибка миграции справочника:', error));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
runMigration();
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "med-cms",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Медицинская мультитендантная CMS на Express и Sequelize (SQLite3) с системой ролей и автоматическим расчетом участков",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
9
|
},
|
|
@@ -10,16 +11,25 @@
|
|
|
10
11
|
"medical",
|
|
11
12
|
"cms",
|
|
12
13
|
"express",
|
|
14
|
+
"sequelize",
|
|
15
|
+
"sqlite3",
|
|
13
16
|
"auth",
|
|
14
17
|
"cookies"
|
|
15
18
|
],
|
|
16
|
-
"author": "
|
|
19
|
+
"author": "prm-programmmer",
|
|
17
20
|
"license": "MIT",
|
|
18
21
|
"dependencies": {
|
|
19
|
-
"
|
|
22
|
+
"bcrypt": "^6.0.0",
|
|
23
|
+
"chalk-palette": "^3.0.0",
|
|
20
24
|
"cookie-parser": "^1.4.6",
|
|
25
|
+
"custom-color-logs": "^3.2.1",
|
|
26
|
+
"dotenv": "^18.0.9",
|
|
21
27
|
"express": "^4.19.2",
|
|
22
28
|
"jsonwebtoken": "^9.0.2",
|
|
23
|
-
"
|
|
29
|
+
"sequelize": "^6.37.3",
|
|
30
|
+
"sqlite3": "^6.0.1"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"node-gyp": "^13.1.0"
|
|
24
34
|
}
|
|
25
35
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
3
|
+
background-color: #f8fafc;
|
|
4
|
+
color: #0f172a;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.admin-panel {
|
|
10
|
+
max-width: 1200px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 30px 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
header {
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
align-items: center;
|
|
19
|
+
border-bottom: 2px solid #e2e8f0;
|
|
20
|
+
padding-bottom: 20px;
|
|
21
|
+
margin-bottom: 30px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
header h1 {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 24px;
|
|
27
|
+
color: #1e3a8a;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.btn-logout {
|
|
31
|
+
background-color: #ef4444;
|
|
32
|
+
color: white;
|
|
33
|
+
border: none;
|
|
34
|
+
padding: 10px 20px;
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
font-weight: bold;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
transition: background 0.2s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.btn-logout:hover {
|
|
42
|
+
background-color: #dc2626;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Карточки аналитики БД */
|
|
46
|
+
.stats-cards {
|
|
47
|
+
display: grid;
|
|
48
|
+
grid-template-columns: repeat(3, 1fr);
|
|
49
|
+
gap: 20px;
|
|
50
|
+
margin-bottom: 40px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.card {
|
|
54
|
+
background: white;
|
|
55
|
+
padding: 20px;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
color: #475569;
|
|
61
|
+
border-top: 4px solid #2563eb;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.card span {
|
|
65
|
+
display: block;
|
|
66
|
+
font-size: 32px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
color: #0f172a;
|
|
69
|
+
margin-top: 10px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Сетка с формами задач */
|
|
73
|
+
.grid-forms {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: 1fr 1fr;
|
|
76
|
+
gap: 30px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.form-box {
|
|
80
|
+
background: white;
|
|
81
|
+
padding: 25px;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.form-box h3 {
|
|
87
|
+
margin-top: 0;
|
|
88
|
+
margin-bottom: 20px;
|
|
89
|
+
color: #0f172a;
|
|
90
|
+
font-size: 18px;
|
|
91
|
+
border-left: 4px solid #2563eb;
|
|
92
|
+
padding-left: 10px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.form-box form input,
|
|
96
|
+
.form-box form select {
|
|
97
|
+
width: 100%;
|
|
98
|
+
padding: 12px;
|
|
99
|
+
margin-bottom: 15px;
|
|
100
|
+
border: 1px solid #cbd5e1;
|
|
101
|
+
border-radius: 6px;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
background-color: #fff;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.form-box form input:focus,
|
|
108
|
+
.form-box form select:focus {
|
|
109
|
+
border-color: #2563eb;
|
|
110
|
+
outline: none;
|
|
111
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.form-box form button {
|
|
115
|
+
width: 100%;
|
|
116
|
+
padding: 12px;
|
|
117
|
+
background-color: #2563eb;
|
|
118
|
+
color: white;
|
|
119
|
+
border: none;
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
font-weight: bold;
|
|
122
|
+
font-size: 15px;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: background 0.2s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.form-box form button:hover {
|
|
128
|
+
background-color: #1d4ed8;
|
|
129
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
3
|
+
background-color: #f0f4f8;
|
|
4
|
+
margin: 0;
|
|
5
|
+
display: flex;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
align-items: center;
|
|
8
|
+
height: 100vh;
|
|
9
|
+
}
|
|
10
|
+
.auth-container {
|
|
11
|
+
background: #ffffff;
|
|
12
|
+
padding: 30px;
|
|
13
|
+
border-radius: 8px;
|
|
14
|
+
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
|
15
|
+
width: 350px;
|
|
16
|
+
}
|
|
17
|
+
.form-group {
|
|
18
|
+
margin-bottom: 15px;
|
|
19
|
+
}
|
|
20
|
+
.form-group label {
|
|
21
|
+
display: block;
|
|
22
|
+
margin-bottom: 5px;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
color: #4a5568;
|
|
25
|
+
}
|
|
26
|
+
.form-group input, textarea, select {
|
|
27
|
+
width: 100%;
|
|
28
|
+
padding: 10px;
|
|
29
|
+
border: 1px solid #cbd5e1;
|
|
30
|
+
border-radius: 4px;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
}
|
|
33
|
+
.btn, .btn-save, button[type="submit"] {
|
|
34
|
+
width: 100%;
|
|
35
|
+
padding: 11px;
|
|
36
|
+
background-color: #2563eb;
|
|
37
|
+
color: white;
|
|
38
|
+
border: none;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
font-weight: bold;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
.btn:hover { background-color: #1d4ed8; }
|
|
44
|
+
.hidden { display: none !important; }
|
|
45
|
+
.toggle-text {
|
|
46
|
+
font-size: 13px;
|
|
47
|
+
text-align: center;
|
|
48
|
+
color: #64748b;
|
|
49
|
+
margin-top: 15px;
|
|
50
|
+
}
|
|
51
|
+
.toggle-text span {
|
|
52
|
+
color: #2563eb;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
text-decoration: underline;
|
|
55
|
+
}
|