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,199 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
loadDashboardStats();
|
|
3
|
+
});
|
|
4
|
+
async function loadDashboardStats() {
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch('/med/api/admin/stats');
|
|
7
|
+
const result = await response.json();
|
|
8
|
+
if (!result.success) {
|
|
9
|
+
alert('Ошибка доступа к админ-панели: ' + result.message);
|
|
10
|
+
window.location.href = '/med/html/login.html';
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
document.getElementById('statClinics').textContent = result.stats.summary.totalClinics;
|
|
14
|
+
document.getElementById('statPatients').textContent = result.stats.summary.totalPatients;
|
|
15
|
+
document.getElementById('statStaff').textContent = result.stats.summary.totalStaff;
|
|
16
|
+
await loadClinicsDropdown();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('Ошибка загрузки статистики:', err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function loadClinicsDropdown() {
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch('/med/api/admin/list-clinics');
|
|
24
|
+
const result = await response.json();
|
|
25
|
+
|
|
26
|
+
const select = document.getElementById('staffClinic');
|
|
27
|
+
select.innerHTML = '<option value="">-- Выберите больницу --</option>';
|
|
28
|
+
|
|
29
|
+
if (result.success && result.data.length > 0) {
|
|
30
|
+
result.data.forEach(clinic => {
|
|
31
|
+
const opt = document.createElement('option');
|
|
32
|
+
opt.value = clinic.id; opt.textContent = `[ID: ${clinic.id}] ${clinic.name}`;
|
|
33
|
+
select.appendChild(opt);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Ошибка наполнения списка клиник:', err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function viewClinicsTable() {
|
|
42
|
+
const block = document.getElementById('tableViewerBlock');
|
|
43
|
+
const title = document.getElementById('tableViewerTitle');
|
|
44
|
+
const content = document.getElementById('tableViewerContent');
|
|
45
|
+
|
|
46
|
+
block.classList.remove('hidden');
|
|
47
|
+
title.textContent = ' 🏥 Реестр медицинских учреждений системы';
|
|
48
|
+
content.innerHTML = '<p>Загрузка таблицы...</p>';
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch('/med/api/admin/list-clinics');
|
|
52
|
+
const result = await res.json();
|
|
53
|
+
|
|
54
|
+
if (result.success && result.data.length > 0) {
|
|
55
|
+
let html = `
|
|
56
|
+
<table>
|
|
57
|
+
<thead>
|
|
58
|
+
<tr>
|
|
59
|
+
<th>ID Клиники</th>
|
|
60
|
+
<th>Название</th>
|
|
61
|
+
<th>Тип</th>
|
|
62
|
+
<th>Адрес</th>
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
`;
|
|
67
|
+
result.data.forEach(c => {
|
|
68
|
+
html += `
|
|
69
|
+
<tr>
|
|
70
|
+
<td><span class="uid-badge">${c.id}</span></td>
|
|
71
|
+
<td><b>${c.name}</b></td>
|
|
72
|
+
<td>${c.type}</td>
|
|
73
|
+
<td>${c.address}</td>
|
|
74
|
+
</tr>
|
|
75
|
+
`;
|
|
76
|
+
});
|
|
77
|
+
html += '</tbody></table>';
|
|
78
|
+
content.innerHTML = html;
|
|
79
|
+
} else {
|
|
80
|
+
content.innerHTML = '<p>В базе данных пока нет созданных учреждений.</p>';
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
content.innerHTML = '<p style="color:red;">Ошибка загрузки данных клиник</p>';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function viewStaffTable() {
|
|
88
|
+
const block = document.getElementById('tableViewerBlock');
|
|
89
|
+
const title = document.getElementById('tableViewerTitle');
|
|
90
|
+
const content = document.getElementById('tableViewerContent');
|
|
91
|
+
|
|
92
|
+
block.classList.remove('hidden');
|
|
93
|
+
title.textContent = '👨⚕️ Зарегистрированный медицинский персонал';
|
|
94
|
+
content.innerHTML = '<p>Загрузка таблицы...</p>';
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch('/med/api/admin/list-staff');
|
|
98
|
+
const result = await res.json();
|
|
99
|
+
|
|
100
|
+
if (result.success && result.data.length > 0) {
|
|
101
|
+
let html = `
|
|
102
|
+
<table>
|
|
103
|
+
<thead>
|
|
104
|
+
<tr>
|
|
105
|
+
<th>ID Сотрудника</th>
|
|
106
|
+
<th>ФИО</th>
|
|
107
|
+
<th>Email</th>
|
|
108
|
+
<th>Должность</th> <!-- Новая колонка -->
|
|
109
|
+
<th>Специализация</th>
|
|
110
|
+
<th>Место работы</th>
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody>
|
|
114
|
+
`;
|
|
115
|
+
result.data.forEach(s => {
|
|
116
|
+
const clinicName = s.Clinic ? s.Clinic.name : '<span style="color:red;">Не привязан</span>';
|
|
117
|
+
html += `
|
|
118
|
+
<tr>
|
|
119
|
+
<td><span class="uid-badge">${s.id}</span></td>
|
|
120
|
+
<td><b>${s.fullName}</b></td>
|
|
121
|
+
<td>${s.email}</td>
|
|
122
|
+
<td><span class="badge" style="background:#f1f5f9; color:#475569;">${s.position || 'Врач'}</span></td> <!-- Выводим должность -->
|
|
123
|
+
<td>${s.specialty || 'Нет'}</td>
|
|
124
|
+
<td>${clinicName}</td>
|
|
125
|
+
</tr>
|
|
126
|
+
`;
|
|
127
|
+
});
|
|
128
|
+
html += '</tbody></table>';
|
|
129
|
+
content.innerHTML = html;
|
|
130
|
+
} else {
|
|
131
|
+
content.innerHTML = '<p>В штате системы пока нет зарегистрированных сотрудников.</p>';
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
content.innerHTML = '<p style="color:red;">Ошибка загрузки данных персонала</p>';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
document.getElementById('clinicForm').addEventListener('submit', async (e) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const payload = {
|
|
140
|
+
name: document.getElementById('clinicName').value,
|
|
141
|
+
type: document.getElementById('clinicType').value,
|
|
142
|
+
address: document.getElementById('clinicAddress').value
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch('/med/api/admin/clinics', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
body: JSON.stringify(payload)
|
|
149
|
+
});
|
|
150
|
+
const result = await response.json();
|
|
151
|
+
if (result.success) {
|
|
152
|
+
alert(`Учреждение успешно добавлено в SQLite под ID №${result.data.id}!`);
|
|
153
|
+
document.getElementById('clinicForm').reset();
|
|
154
|
+
await loadDashboardStats();
|
|
155
|
+
} else {
|
|
156
|
+
alert('Ошибка: ' + result.error);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
alert('Ошибка сети при создании клиники');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
document.getElementById('staffForm').addEventListener('submit', async (e) => {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const clinicSelect = document.getElementById('staffClinic');
|
|
165
|
+
const selectedClinicId = clinicSelect.value;
|
|
166
|
+
const selectedProfile = document.getElementById('staffMedicalProfile').value;
|
|
167
|
+
if (!selectedClinicId) {
|
|
168
|
+
alert('Пожалуйста, выберите больницу из списка!');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const payload = {
|
|
172
|
+
fullName: document.getElementById('staffName').value,
|
|
173
|
+
email: document.getElementById('staffEmail').value,
|
|
174
|
+
rawPassword: document.getElementById('staffPassword').value,
|
|
175
|
+
medicalProfile: selectedProfile,
|
|
176
|
+
clinicId: parseInt(selectedClinicId, 10)
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch('/med/api/admin/staff', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(payload)
|
|
183
|
+
});
|
|
184
|
+
const result = await response.json();
|
|
185
|
+
if (result.success) {
|
|
186
|
+
alert(`Сотрудник успешно создан!\nПрофиль: ${selectedProfile}`);
|
|
187
|
+
document.getElementById('staffForm').reset();
|
|
188
|
+
await loadDashboardStats();
|
|
189
|
+
} else {
|
|
190
|
+
alert('Ошибка создания: ' + result.error);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
alert('Ошибка сети при создании сотрудника');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
async function logout() {
|
|
197
|
+
await fetch('/med/api/auth/logout', { method: 'POST' });
|
|
198
|
+
window.location.href = '/med/med-login';
|
|
199
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function toggleAuth() {
|
|
2
|
+
document.getElementById('loginBox').classList.toggle('hidden');
|
|
3
|
+
document.getElementById('registerBox').classList.toggle('hidden');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
const email = document.getElementById('loginEmail').value;
|
|
9
|
+
const password = document.getElementById('loginPassword').value;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('/med/api/auth/login', {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ email, password })
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const result = await response.json();
|
|
19
|
+
|
|
20
|
+
if (result.success) {
|
|
21
|
+
if (result.user.role === 'admin') window.location.href = '/med/html/dashboard-admin.html';
|
|
22
|
+
else if (result.user.role === 'doctor') window.location.href = '/med/html/dashboard-doctor.html';
|
|
23
|
+
else window.location.href = '/med/html/dashboard-patient.html';
|
|
24
|
+
} else {
|
|
25
|
+
alert('Ошибка: ' + result.message);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
alert('Не удалось связаться с сервером');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
const fullName = document.getElementById('regName').value;
|
|
35
|
+
const email = document.getElementById('regEmail').value;
|
|
36
|
+
const password = document.getElementById('regPassword').value;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('/med/api/auth/register', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ email, password, fullName })
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await response.json();
|
|
46
|
+
if (result.success) {
|
|
47
|
+
alert('Регистрация прошла успешно! Теперь вы можете войти.');
|
|
48
|
+
toggleAuth();
|
|
49
|
+
} else {
|
|
50
|
+
alert('Ошибка регистрации: ' + result.message);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
alert('Ошибка сети');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
loadSchedule();
|
|
3
|
+
loadDiseasesCatalog();
|
|
4
|
+
loadDoctorProfile();
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
async function loadDiseasesCatalog() {
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch('/med/api/doctor/diseases-list');
|
|
10
|
+
const result = await res.json();
|
|
11
|
+
|
|
12
|
+
const select = document.getElementById('examDisease');
|
|
13
|
+
|
|
14
|
+
select.innerHTML = '<option value="">-- Выберите диагноз из справочника МКБ --</option>';
|
|
15
|
+
|
|
16
|
+
if (result.success && result.data.length > 0) {
|
|
17
|
+
result.data.forEach(disease => {
|
|
18
|
+
const opt = document.createElement('option');
|
|
19
|
+
opt.value = disease.id; opt.textContent = `[${disease.code}] ${disease.name}`; select.appendChild(opt);
|
|
20
|
+
});
|
|
21
|
+
console.log(`🌱 [АРМ Врача] Успешно загружено ${result.data.length} диагнозов из SQLite таблицы.`);
|
|
22
|
+
} else {
|
|
23
|
+
select.innerHTML = '<option value="">Справочник болезней пуст (Запустите миграцию)</option>';
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('❌ Ошибка при получении справочника болезней:', err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadSchedule() {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch('/med/api/doctor/schedule');
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
|
|
35
|
+
const list = document.getElementById('scheduleList');
|
|
36
|
+
list.innerHTML = '';
|
|
37
|
+
|
|
38
|
+
if (data.success && data.schedule && data.schedule.length > 0) {
|
|
39
|
+
data.schedule.forEach(ap => {
|
|
40
|
+
const li = document.createElement('li');
|
|
41
|
+
li.innerHTML = `<b>${ap.time}</b> - ${ap.Patient.fullName}
|
|
42
|
+
<button onclick="startExamination('${ap.id}', '${ap.Patient.fullName}')" class="btn-action">Принять</button>`;
|
|
43
|
+
list.appendChild(li);
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
list.innerHTML = '<li>Ближайших записей на сегодня нет</li>';
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Ошибка загрузки расписания:', err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
document.getElementById('examForm').addEventListener('submit', async (e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
|
|
56
|
+
const selectedDiseaseId = document.getElementById('examDisease').value;
|
|
57
|
+
|
|
58
|
+
const payload = {
|
|
59
|
+
appointmentId: document.getElementById('activeAppointmentId').value,
|
|
60
|
+
complaints: document.getElementById('examComplaints').value,
|
|
61
|
+
objectively: document.getElementById('examObjective').value,
|
|
62
|
+
doctorNotes: document.getElementById('examNotes').value,
|
|
63
|
+
diseaseId: selectedDiseaseId ? parseInt(selectedDiseaseId, 10) : null
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch('/med/api/doctor/complete-examination', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(payload)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await res.json();
|
|
74
|
+
if (result.success) {
|
|
75
|
+
alert('Прием успешно завершен! Карточка обновлена, статистика внесена в SQLite.');
|
|
76
|
+
document.getElementById('examBlock').classList.add('hidden');
|
|
77
|
+
document.getElementById('examForm').reset();
|
|
78
|
+
await loadSchedule(); } else {
|
|
79
|
+
alert('Ошибка завершения приема: ' + result.error);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
alert('Ошибка сети при отправке данных осмотра');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function startExamination(appointmentId, patientName) {
|
|
87
|
+
document.getElementById('examBlock').classList.remove('hidden');
|
|
88
|
+
document.getElementById('activeAppointmentId').value = appointmentId;
|
|
89
|
+
document.getElementById('currentPatientName').textContent = patientName;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function logout() {
|
|
93
|
+
await fetch('/med/api/auth/logout', { method: 'POST' });
|
|
94
|
+
window.location.href = '/med/med-login';
|
|
95
|
+
}
|
|
96
|
+
async function loadDoctorProfile() {
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch('/med/api/doctor/me');
|
|
99
|
+
const result = await res.json();
|
|
100
|
+
if (result.success && result.doctor) {
|
|
101
|
+
document.getElementById('doctorName').textContent = result.doctor.fullName;
|
|
102
|
+
document.getElementById('doctorPosition').textContent = result.doctor.position || 'Врач';
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('Ошибка загрузки профиля сотрудника:', err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function togglePasswordModal() {
|
|
110
|
+
const modal = document.getElementById('passwordModal');
|
|
111
|
+
modal.classList.toggle('hidden');
|
|
112
|
+
document.getElementById('changePasswordForm').reset();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
document.getElementById('changePasswordForm').addEventListener('submit', async (e) => {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
|
|
118
|
+
const oldPassword = document.getElementById('oldPassword').value;
|
|
119
|
+
const newPassword = document.getElementById('newPassword').value;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch('/med/api/auth/change-password', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ oldPassword, newPassword })
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = await response.json();
|
|
129
|
+
if (result.success) {
|
|
130
|
+
alert('Ваш пароль успешно изменен в базе данных SQLite!');
|
|
131
|
+
togglePasswordModal(); } else {
|
|
132
|
+
alert('Ошибка смены пароля: ' + result.message);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
alert('Ошибка сети при попытке изменить пароль');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
loadPatientData();
|
|
3
|
+
loadDoctorsForBooking();
|
|
4
|
+
});
|
|
5
|
+
async function loadPatientData() {
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch('/med/api/patient/me');
|
|
8
|
+
const result = await response.json();
|
|
9
|
+
if (!result.success) {
|
|
10
|
+
alert('Ошибка загрузки данных: ' + result.message);
|
|
11
|
+
window.location.href = '/med/html/login.html';
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const data = result.data;
|
|
15
|
+
document.getElementById('patientName').textContent = data.personalInfo.fullName;
|
|
16
|
+
document.getElementById('patientEmail').textContent = data.personalInfo.email;
|
|
17
|
+
const districtInfo = document.getElementById('districtInfo');
|
|
18
|
+
if (data.personalInfo.UserAddress) {
|
|
19
|
+
const addr = data.personalInfo.UserAddress;
|
|
20
|
+
districtInfo.innerHTML = `
|
|
21
|
+
<p><b>Ваш адрес:</b> г. ${addr.city}, ул. ${addr.street}, д. ${addr.houseNumber}</p>
|
|
22
|
+
<p style="margin-top: 10px;">🩺 <span class="badge">${addr.assignedDistrict}</span></p>
|
|
23
|
+
<p style="margin-top: 10px;">🚪 <span class="badge" style="background:#ecfdf5; color:#065f46;">${addr.assignedCabinet}</span></p>
|
|
24
|
+
`;
|
|
25
|
+
document.getElementById('addrCity').value = addr.city;
|
|
26
|
+
document.getElementById('addrStreet').value = addr.street;
|
|
27
|
+
document.getElementById('addrHouse').value = addr.houseNumber;
|
|
28
|
+
}
|
|
29
|
+
const historyContainer = document.getElementById('medicalHistoryList');
|
|
30
|
+
historyContainer.innerHTML = '';
|
|
31
|
+
if (data.medicalHistory && data.medicalHistory.length > 0) {
|
|
32
|
+
data.medicalHistory.forEach(record => {
|
|
33
|
+
const card = document.createElement('div');
|
|
34
|
+
if (record.status === 'ожидается') {
|
|
35
|
+
card.className = 'record-card future';
|
|
36
|
+
card.innerHTML = `
|
|
37
|
+
<div style="display:flex; justify-content:space-between; font-weight:bold; color:#047857;">
|
|
38
|
+
<span>📅 Предстоящий прием: ${record.date} в ${record.time}</span>
|
|
39
|
+
<span>Статус: ожидается</span>
|
|
40
|
+
</div>
|
|
41
|
+
<p style="margin: 5px 0 0 0;"><b>Врач:</b> ${record.Doctor.fullName} (${record.Doctor.specialty})</p>
|
|
42
|
+
`;
|
|
43
|
+
} else if (record.status === 'завершен') {
|
|
44
|
+
card.className = 'record-card';
|
|
45
|
+
card.innerHTML = `
|
|
46
|
+
<div style="display:flex; justify-content:space-between; font-weight:bold; color:#1e3a8a; margin-bottom: 8px;">
|
|
47
|
+
<span>🩺 Осмотр от ${record.date} в ${record.time}</span>
|
|
48
|
+
<span style="color:#2563eb;">Диагноз: [${record.Disease.code}] ${record.Disease.name}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<p style="margin: 4px 0;"><b>Врач:</b> ${record.Doctor.fullName} (${record.Doctor.specialty})</p>
|
|
51
|
+
<p style="margin: 4px 0; font-size:14px;"><b>Жалобы:</b> ${record.complaints || '—'}</p>
|
|
52
|
+
<p style="margin: 4px 0; font-size:14px;"><b>Данные осмотра:</b> ${record.objectively || '—'}</p>
|
|
53
|
+
<div style="margin-top:8px; padding:8px; background:#f1f5f9; border-radius:4px; font-size:13px;">
|
|
54
|
+
<p style="margin:0 0 4px 0;">💊 <b>Рекомендации и лекарства:</b> ${record.Disease.treatmentRecommendations}</p>
|
|
55
|
+
<p style="margin:0;"><b>Назначено:</b> ${record.Disease.recommendedMedications}</p>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
historyContainer.appendChild(card);
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
historyContainer.innerHTML = '<p style="color: #64748b;">У вас пока нет записей в медицинской карте.</p>';
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Ошибка связи с сервером:', err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
document.getElementById('addressForm').addEventListener('submit', async (e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
const payload = {
|
|
71
|
+
city: document.getElementById('addrCity').value,
|
|
72
|
+
street: document.getElementById('addrStreet').value,
|
|
73
|
+
houseNumber: document.getElementById('addrHouse').value
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch('/med/api/patient/address', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify(payload)
|
|
80
|
+
});
|
|
81
|
+
const result = await response.json();
|
|
82
|
+
if (result.success) {
|
|
83
|
+
alert('Адрес сохранен! Система автоматически пересчитала ваш лечебный участок.');
|
|
84
|
+
loadPatientData();
|
|
85
|
+
} else {
|
|
86
|
+
alert('Ошибка: ' + result.error);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
alert('Ошибка сети при отправке адреса');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
async function logout() {
|
|
93
|
+
await fetch('/med-api/auth/logout', { method: 'POST' });
|
|
94
|
+
window.location.href = '/med/html/login.html';
|
|
95
|
+
}
|
|
96
|
+
async function loadDoctorsForBooking() {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch('/med/api/patient/doctors-list');
|
|
99
|
+
const result = await response.json();
|
|
100
|
+
const select = document.getElementById('bookingDoctor');
|
|
101
|
+
select.innerHTML = '<option value="">-- Выберите врача --</option>';
|
|
102
|
+
if (result.success && result.data.length > 0) {
|
|
103
|
+
result.data.forEach(doc => {
|
|
104
|
+
const opt = document.createElement('option');
|
|
105
|
+
opt.value = doc.id;
|
|
106
|
+
const clinicInfo = doc.Clinic ? ` (${doc.Clinic.name})` : '';
|
|
107
|
+
opt.textContent = `${doc.specialty} — ${doc.fullName}${clinicInfo}`;
|
|
108
|
+
select.appendChild(opt);
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
select.innerHTML = '<option value="">Врачи пока не зарегистрированы в CMS</option>';
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('Ошибка загрузки врачей:', err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
document.getElementById('bookingForm').addEventListener('submit', async (e) => {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
const payload = {
|
|
120
|
+
doctorId: document.getElementById('bookingDoctor').value,
|
|
121
|
+
date: document.getElementById('bookingDate').value,
|
|
122
|
+
time: document.getElementById('bookingTime').value
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch('/med/api/patient/book', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify(payload)
|
|
129
|
+
});
|
|
130
|
+
const result = await response.json();
|
|
131
|
+
if (result.success) {
|
|
132
|
+
alert('Вы успешно записаны на прием! Талон добавлен в вашу медицинскую карту.');
|
|
133
|
+
document.getElementById('bookingForm').reset();
|
|
134
|
+
await loadPatientData();
|
|
135
|
+
} else {
|
|
136
|
+
alert('Ошибка бронирования: ' + result.error);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
alert('Ошибка сети при попытке записи');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const { SPECIALTY_PERMISSIONS } = require('./permissions');
|
|
3
|
+
|
|
4
|
+
module.exports = function(jwtSecret) {
|
|
5
|
+
return {
|
|
6
|
+
/**
|
|
7
|
+
* Основная проверка авторизации по HttpOnly куке
|
|
8
|
+
*/
|
|
9
|
+
verifyCookie: (req, res, next) => {
|
|
10
|
+
const token = req.cookies.med_auth_token;
|
|
11
|
+
|
|
12
|
+
if (!token) {
|
|
13
|
+
return res.status(401).json({ success: false, message: 'Доступ запрещен. Пожалуйста, войдите в систему.' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const decoded = jwt.verify(token, jwtSecret);
|
|
18
|
+
req.user = decoded; // Добавляем в req: id, role, specialty
|
|
19
|
+
next();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return res.status(401).json({ success: false, message: 'Ваша сессия устарела. Войдите заново.' });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Проверка глобальной роли (admin, doctor, patient)
|
|
27
|
+
*/
|
|
28
|
+
requireRole: (allowedRoles) => {
|
|
29
|
+
return (req, res, next) => {
|
|
30
|
+
if (!req.user || !allowedRoles.includes(req.user.role)) {
|
|
31
|
+
return res.status(403).json({ success: false, message: 'Недостаточно прав для выполнения этого действия.' });
|
|
32
|
+
}
|
|
33
|
+
next();
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Проверка специфического медицинского действия на основе специальности врача
|
|
39
|
+
*/
|
|
40
|
+
checkPermission: (requiredPermission) => {
|
|
41
|
+
return (req, res, next) => {
|
|
42
|
+
// Администраторам разрешаем любые медицинские эндпоинты по дефолту
|
|
43
|
+
if (req.user && req.user.role === 'admin') {
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const doctorSpecialty = req.user?.specialty;
|
|
48
|
+
if (!doctorSpecialty) {
|
|
49
|
+
return res.status(403).json({ success: false, message: 'Действие доступно только медицинскому персоналу.' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const allowedPermissions = SPECIALTY_PERMISSIONS[doctorSpecialty] || [];
|
|
53
|
+
|
|
54
|
+
if (!allowedPermissions.includes(requiredPermission)) {
|
|
55
|
+
return res.status(403).json({
|
|
56
|
+
success: false,
|
|
57
|
+
message: `У специальности "${doctorSpecialty}" нет прав на действие: ${requiredPermission}`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
next();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const MED_PERMISSIONS = {
|
|
2
|
+
WRITE_RECIPE: 'write_recipe',
|
|
3
|
+
WRITE_SICK_LEAVE: 'write_sick_leave',
|
|
4
|
+
SCHEDULE_SURGERY: 'schedule_surgery',
|
|
5
|
+
UPLOAD_SCAN_RESULTS: 'upload_scan_results',
|
|
6
|
+
WRITE_PSYCH_REPORT: 'write_psych_report',
|
|
7
|
+
WRITE_AUTOPSY_REPORT: 'write_autopsy',
|
|
8
|
+
GYNECOLOGY_EXAM: 'gynecology_exam',
|
|
9
|
+
PRESCRIBE_GLASSES: 'prescribe_glasses',
|
|
10
|
+
};
|
|
11
|
+
const SPECIALTY_PERMISSIONS = {
|
|
12
|
+
'Терапевт': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
13
|
+
'Педиатр': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
14
|
+
'Врач общей практики': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
15
|
+
'Кардиолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
16
|
+
'Невролог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
17
|
+
'Эндокринолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
18
|
+
'Гастроэнтеролог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
19
|
+
'Пульмонолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
20
|
+
'Нефролог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
21
|
+
'Ревматолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
22
|
+
'Гематолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
23
|
+
'Онколог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
24
|
+
'Инфекционист': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
25
|
+
'Аллерголог-иммунолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
26
|
+
'Дерматолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
27
|
+
'Уролог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
28
|
+
'Хирург': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
29
|
+
'Травматолог-ортопед': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
30
|
+
'Сосудистый хирург': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
31
|
+
'Нейрохирург': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
32
|
+
'Кардиохирург': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
33
|
+
'Пластический хирург': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.SCHEDULE_SURGERY],
|
|
34
|
+
'Офтальмолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.PRESCRIBE_GLASSES],
|
|
35
|
+
'Отоларинголог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
36
|
+
'Стоматолог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE],
|
|
37
|
+
'Гинеколог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.GYNECOLOGY_EXAM],
|
|
38
|
+
'Психиатр': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.WRITE_PSYCH_REPORT],
|
|
39
|
+
'Психотерапевт': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.WRITE_PSYCH_REPORT],
|
|
40
|
+
'Нарколог': [MED_PERMISSIONS.WRITE_RECIPE, MED_PERMISSIONS.WRITE_SICK_LEAVE, MED_PERMISSIONS.WRITE_PSYCH_REPORT],
|
|
41
|
+
'Рентгенолог': [MED_PERMISSIONS.UPLOAD_SCAN_RESULTS],
|
|
42
|
+
'Врач УЗД': [MED_PERMISSIONS.UPLOAD_SCAN_RESULTS],
|
|
43
|
+
'Эндоскопист': [MED_PERMISSIONS.UPLOAD_SCAN_RESULTS],
|
|
44
|
+
'Анестезиолог-реаниматолог': [MED_PERMISSIONS.WRITE_RECIPE],
|
|
45
|
+
'Патологоанатом': [MED_PERMISSIONS.WRITE_AUTOPSY_REPORT]
|
|
46
|
+
};
|
|
47
|
+
module.exports = { MED_PERMISSIONS, SPECIALTY_PERMISSIONS };
|