All checks were successful
continuous-integration/drone/push Build is passing
Changes: - Fix nutrition service: add is_active column and Pydantic validation for UUID/datetime - Add location-based alerts feature: users can now see alerts within 1km radius - Fix CORS and response serialization in nutrition service - Add getCurrentLocation() and loadAlertsNearby() functions - Improve UI for nearby alerts display with distance and response count
1592 lines
62 KiB
HTML
1592 lines
62 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' http://localhost:*; connect-src 'self' http://localhost:* https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;">
|
||
<title>👩💼 Women's Safety App - Полный функционал</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
color: #333;
|
||
}
|
||
|
||
.app-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 250px;
|
||
background: white;
|
||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||
overflow-y: auto;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px;
|
||
border-bottom: 2px solid #f0f0f0;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.sidebar-header h1 {
|
||
font-size: 1.3em;
|
||
color: #667eea;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.sidebar-header p {
|
||
font-size: 0.85em;
|
||
color: #999;
|
||
}
|
||
|
||
.user-info {
|
||
padding: 15px 20px;
|
||
background: #f5f5f5;
|
||
margin: 0 10px 20px;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
|
||
.user-info .name {
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.user-info .email {
|
||
font-size: 0.85em;
|
||
color: #999;
|
||
}
|
||
|
||
.nav-section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.nav-section-title {
|
||
padding: 10px 20px;
|
||
font-size: 0.75em;
|
||
font-weight: bold;
|
||
color: #999;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.nav-item {
|
||
padding: 12px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border-left: 4px solid transparent;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: #f0f0f0;
|
||
color: #667eea;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: #f0f0f0;
|
||
border-left-color: #667eea;
|
||
color: #667eea;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f8f9fa;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: white;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.header h2 {
|
||
color: #667eea;
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
.logout-btn {
|
||
padding: 10px 20px;
|
||
background: #f44336;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: #d32f2f;
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 30px;
|
||
}
|
||
|
||
.page {
|
||
display: none;
|
||
}
|
||
|
||
.page.active {
|
||
display: block;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 25px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
|
||
.card h3 {
|
||
color: #667eea;
|
||
margin-bottom: 20px;
|
||
font-size: 1.3em;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
input, textarea, select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 1em;
|
||
font-family: inherit;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
input:focus, textarea:focus, select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 10px rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
button {
|
||
padding: 12px 24px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
font-size: 1em;
|
||
transition: all 0.2s;
|
||
width: 100%;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin-bottom: 15px;
|
||
border-left: 4px solid;
|
||
}
|
||
|
||
.alert.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border-color: #4caf50;
|
||
}
|
||
|
||
.alert.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border-color: #f44336;
|
||
}
|
||
|
||
.alert.info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.alert.warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
border-color: #ff9800;
|
||
}
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.9em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.list {
|
||
list-style: none;
|
||
}
|
||
|
||
.list-item {
|
||
background: white;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
border-radius: 5px;
|
||
border-left: 4px solid #667eea;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.list-item-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.list-item-title {
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.list-item-desc {
|
||
font-size: 0.9em;
|
||
color: #999;
|
||
}
|
||
|
||
.list-item-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 8px 15px;
|
||
font-size: 0.9em;
|
||
width: auto;
|
||
margin: 0;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #4caf50;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #ff9800;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
padding: 30px;
|
||
border-radius: 10px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
color: #667eea;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 2em;
|
||
cursor: pointer;
|
||
color: #999;
|
||
padding: 0;
|
||
width: auto;
|
||
margin: 0;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #333;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 5px 10px;
|
||
border-radius: 20px;
|
||
font-size: 0.85em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-active {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status-resolved {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
|
||
.status-inactive {
|
||
background: #f5f5f5;
|
||
color: #999;
|
||
}
|
||
|
||
.login-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
.login-form {
|
||
background: white;
|
||
padding: 40px;
|
||
border-radius: 10px;
|
||
width: 100%;
|
||
max-width: 400px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.login-form h1 {
|
||
color: #667eea;
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
font-size: 2em;
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
min-height: 100px;
|
||
}
|
||
|
||
.loading {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.app-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 100%;
|
||
max-height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
padding: 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
margin-bottom: 0;
|
||
border-bottom: none;
|
||
border-right: 2px solid #f0f0f0;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.nav-section {
|
||
display: flex;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Login Page -->
|
||
<div id="loginPage" class="login-container">
|
||
<div class="login-form">
|
||
<h1>👩💼 Women's Safety</h1>
|
||
<div id="loginAlert"></div>
|
||
<div class="form-group">
|
||
<label>Email</label>
|
||
<input type="email" id="loginEmail" placeholder="user@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Пароль</label>
|
||
<input type="password" id="loginPassword" placeholder="Ваш пароль">
|
||
</div>
|
||
<button id="loginBtn">🔓 Войти</button>
|
||
<div style="text-align: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
|
||
<p style="color: #999; font-size: 0.9em; margin-bottom: 10px;">Нет учетной записи? Создать новую</p>
|
||
<button id="registerBtn" style="background: #f0f0f0; color: #667eea;">✍️ Регистрация</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Register Modal -->
|
||
<div id="registerModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>✍️ Регистрация</h2>
|
||
<button class="close-btn" onclick="closeModal('registerModal')">✕</button>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Email</label>
|
||
<input type="email" id="regEmail" placeholder="user@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Пароль (минимум 8 символов)</label>
|
||
<input type="password" id="regPassword" placeholder="Надежный пароль">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Полное имя</label>
|
||
<input type="text" id="regName" placeholder="Ваше имя">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Номер телефона</label>
|
||
<input type="tel" id="regPhone" placeholder="+7 (999) 999-99-99">
|
||
</div>
|
||
<button onclick="register()">✍️ Создать аккаунт</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main App -->
|
||
<div id="appPage" style="display: none;" class="app-container">
|
||
<!-- Sidebar -->
|
||
<div class="sidebar">
|
||
<div class="sidebar-header">
|
||
<h1>👩💼 Safety</h1>
|
||
<p>Приложение безопасности</p>
|
||
</div>
|
||
<div id="userInfoSection" class="user-info">
|
||
<div class="name" id="userNameDisplay">Загрузка...</div>
|
||
<div class="email" id="userEmailDisplay">user@example.com</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-section-title">👤 Профиль</div>
|
||
<div class="nav-item active" onclick="navigate('profile')">📋 Профиль</div>
|
||
<div class="nav-item" onclick="navigate('settings')">⚙️ Настройки</div>
|
||
<div class="nav-item" onclick="navigate('contacts')">👥 Экстренные контакты</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-section-title">🚨 Экстренное</div>
|
||
<div class="nav-item" onclick="navigate('sos')">🚨 SOS Сигнал</div>
|
||
<div class="nav-item" onclick="navigate('alerts')">🔔 Мои алерты</div>
|
||
<div class="nav-item" onclick="navigate('responses')">✅ Ответы</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-section-title">📅 Здоровье</div>
|
||
<div class="nav-item" onclick="navigate('calendar')">📅 Мой календарь</div>
|
||
<div class="nav-item" onclick="navigate('nutrition')">🥗 Питание</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-section-title">📍 Социум</div>
|
||
<div class="nav-item" onclick="navigate('location')">📍 Рядом</div>
|
||
<div class="nav-item" onclick="navigate('safety')">🛡️ Безопасные зоны</div>
|
||
</div>
|
||
|
||
<div style="padding: 20px;">
|
||
<button class="logout-btn" onclick="logout()">🚪 Выход</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="main-content">
|
||
<div class="header">
|
||
<h2 id="pageTitle">📋 Профиль</h2>
|
||
<div></div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<!-- Profile Page -->
|
||
<div id="profile" class="page active">
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h3>👤 Мой профиль</h3>
|
||
<div id="profileInfo" style="color: #666; line-height: 1.8;">
|
||
Загрузка...
|
||
</div>
|
||
<button onclick="showEditProfile()">✏️ Редактировать</button>
|
||
</div>
|
||
<div class="card">
|
||
<h3>📊 Статистика</h3>
|
||
<div class="stats">
|
||
<div class="stat">
|
||
<div class="stat-value" id="totalAlerts">0</div>
|
||
<div class="stat-label">Всего алертов</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="activeAlerts">0</div>
|
||
<div class="stat-label">Активные</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SOS Page -->
|
||
<div id="sos" class="page">
|
||
<div class="card">
|
||
<h3>🚨 Создать SOS Сигнал</h3>
|
||
<div class="form-group">
|
||
<label>Тип чрезвычайной ситуации</label>
|
||
<select id="sosType">
|
||
<option value="violence">🚨 Насилие</option>
|
||
<option value="harassment">😠 Преследование</option>
|
||
<option value="medical">🏥 Медицинская помощь</option>
|
||
<option value="unsafe_area">⚠️ Опасная зона</option>
|
||
<option value="accident">🚗 Авария</option>
|
||
<option value="fire">🔥 Пожар</option>
|
||
<option value="general">📍 Другое</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Широта</label>
|
||
<input type="number" id="sosLat" placeholder="55.7558" value="55.7558" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Долгота</label>
|
||
<input type="number" id="sosLon" placeholder="37.6173" value="37.6173" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Адрес</label>
|
||
<input type="text" id="sosAddress" placeholder="Укажите адрес" value="Москва">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Описание ситуации</label>
|
||
<textarea id="sosMessage" placeholder="Опишите ситуацию...">Требуется немедленная помощь</textarea>
|
||
</div>
|
||
<button onclick="createSOS()">🚨 ОТПРАВИТЬ SOS</button>
|
||
<div id="sosAlert"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alerts Page -->
|
||
<div id="alerts" class="page">
|
||
<div class="card">
|
||
<h3>🔔 Мои алерты</h3>
|
||
<div style="margin-bottom: 20px;">
|
||
<button onclick="loadAlerts()" style="margin-right: 10px;">🔄 Обновить</button>
|
||
<button onclick="loadAlertsNearby()" style="background-color: #ff9800;">📍 Алерты рядом (1км)</button>
|
||
</div>
|
||
<div id="nearbyAlertsInfo" style="margin-bottom: 20px; padding: 10px; background: #f0f0f0; border-radius: 5px; display: none;">
|
||
<div id="nearbyAlertsList"></div>
|
||
</div>
|
||
<ul class="list" id="alertsList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Загрузка алертов...</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Calendar Page -->
|
||
<div id="calendar" class="page">
|
||
<div class="card">
|
||
<h3>📅 Женский календарь</h3>
|
||
<div class="form-group">
|
||
<label>Дата</label>
|
||
<input type="date" id="calendarDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Тип записи</label>
|
||
<select id="calendarType">
|
||
<option value="period">Критические дни</option>
|
||
<option value="ovulation">Овуляция</option>
|
||
<option value="symptoms">Симптомы</option>
|
||
<option value="medication">Лекарства</option>
|
||
<option value="mood">Настроение</option>
|
||
<option value="exercise">Упражнения</option>
|
||
<option value="appointment">Прием врача</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Самочувствие</label>
|
||
<select id="calendarMood">
|
||
<option value="great">😊 Отличное</option>
|
||
<option value="good">🙂 Хорошее</option>
|
||
<option value="normal">😐 Нормальное</option>
|
||
<option value="bad">😕 Плохое</option>
|
||
<option value="terrible">😢 Ужасное</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Заметки</label>
|
||
<textarea id="calendarNotes" placeholder="Добавьте заметки..."></textarea>
|
||
</div>
|
||
<button onclick="addCalendarEntry()">➕ Добавить запись</button>
|
||
<div id="calendarAlert"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>📋 История</h3>
|
||
<ul class="list" id="calendarList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Загрузка...</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nutrition Page -->
|
||
<div id="nutrition" class="page">
|
||
<div class="card">
|
||
<h3>🥗 Питание</h3>
|
||
<div class="form-group">
|
||
<label>Дата</label>
|
||
<input type="date" id="nutritionDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Блюдо/Продукт</label>
|
||
<input type="text" id="nutritionDish" placeholder="Например: омлет с беконом">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Описание</label>
|
||
<textarea id="nutritionDesc" placeholder="Состав, калории и т.д."></textarea>
|
||
</div>
|
||
<button onclick="addNutrition()">➕ Добавить</button>
|
||
<div id="nutritionAlert"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contacts Page -->
|
||
<div id="contacts" class="page">
|
||
<div class="card">
|
||
<h3>👥 Экстренные контакты</h3>
|
||
<div class="form-group">
|
||
<label>Имя</label>
|
||
<input type="text" id="contactName" placeholder="Имя контакта">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Телефон</label>
|
||
<input type="tel" id="contactPhone" placeholder="+7 (999) 999-99-99">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Отношение</label>
|
||
<input type="text" id="contactRelation" placeholder="Мама, подруга, адвокат и т.д.">
|
||
</div>
|
||
<button onclick="addContact()">➕ Добавить контакт</button>
|
||
<div id="contactsAlert"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>📋 Список контактов</h3>
|
||
<ul class="list" id="contactsList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Загрузка...</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Page -->
|
||
<div id="settings" class="page">
|
||
<div class="card">
|
||
<h3>⚙️ Настройки приложения</h3>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" checked> Получать push-уведомления
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" checked> Поделиться локацией с контактами
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" checked> Звуковые уведомления
|
||
</label>
|
||
</div>
|
||
<button onclick="saveSettings()">💾 Сохранить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Location Page -->
|
||
<div id="location" class="page">
|
||
<div class="card">
|
||
<h3>📍 Пользователи рядом</h3>
|
||
<button onclick="loadNearbyUsers()">🔄 Обновить</button>
|
||
<ul class="list" id="nearbyList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Загрузка...</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Safety Zones Page -->
|
||
<div id="safety" class="page">
|
||
<div class="card">
|
||
<h3>🛡️ Безопасные зоны</h3>
|
||
<ul class="list" id="safetyList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Полицейские участки</div>
|
||
<div class="list-item-desc">Расстояние: 500м</div>
|
||
</div>
|
||
</li>
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Больницы</div>
|
||
<div class="list-item-desc">Расстояние: 1.2км</div>
|
||
</div>
|
||
</li>
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Пожарные части</div>
|
||
<div class="list-item-desc">Расстояние: 800м</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Responses Page -->
|
||
<div id="responses" class="page">
|
||
<div class="card">
|
||
<h3>✅ Ответы на мои алерты</h3>
|
||
<ul class="list" id="responsesList">
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">Загрузка...</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Configuration
|
||
const API_CONFIG = {
|
||
userService: 'http://localhost:8001',
|
||
emergencyService: 'http://localhost:8002',
|
||
locationService: 'http://localhost:8003',
|
||
calendarService: 'http://localhost:8004',
|
||
notificationService: 'http://localhost:8005',
|
||
nutritionService: 'http://localhost:8006'
|
||
};
|
||
|
||
// Global state
|
||
let appState = {
|
||
token: localStorage.getItem('token') || null,
|
||
user: null,
|
||
userId: null,
|
||
isLoggedIn: false
|
||
};
|
||
|
||
// Initialize
|
||
window.addEventListener('DOMContentLoaded', init);
|
||
|
||
function init() {
|
||
if (appState.token) {
|
||
showApp();
|
||
loadUserProfile();
|
||
loadAlerts();
|
||
} else {
|
||
showLogin();
|
||
}
|
||
|
||
// Attach event listeners
|
||
const loginBtn = document.getElementById('loginBtn');
|
||
if (loginBtn) {
|
||
loginBtn.addEventListener('click', login);
|
||
}
|
||
|
||
const registerBtn = document.getElementById('registerBtn');
|
||
if (registerBtn) {
|
||
registerBtn.addEventListener('click', showRegister);
|
||
}
|
||
}
|
||
|
||
// Auth functions
|
||
async function login() {
|
||
const email = document.getElementById('loginEmail').value;
|
||
const password = document.getElementById('loginPassword').value;
|
||
|
||
if (!email || !password) {
|
||
showAlert('loginAlert', '❌ Заполните все поля', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAlert('loginAlert', '⏳ Авторизация...', 'info');
|
||
|
||
const res = await fetch(`${API_CONFIG.userService}/api/v1/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const text = await res.text();
|
||
console.error('Login error:', res.status, text);
|
||
throw new Error(`Ошибка ${res.status}: ${text.substring(0, 100)}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
if (!data.access_token) {
|
||
throw new Error('Токен не получен');
|
||
}
|
||
|
||
appState.token = data.access_token;
|
||
|
||
// Parse JWT to get user_id
|
||
const parts = appState.token.split('.');
|
||
if (parts.length === 3) {
|
||
const payload = JSON.parse(atob(parts[1]));
|
||
appState.userId = payload.sub;
|
||
}
|
||
|
||
localStorage.setItem('token', appState.token);
|
||
localStorage.setItem('userId', appState.userId);
|
||
appState.isLoggedIn = true;
|
||
|
||
showAlert('loginAlert', '✅ Успешно!', 'success');
|
||
setTimeout(() => {
|
||
showApp();
|
||
loadUserProfile();
|
||
}, 500);
|
||
} catch (e) {
|
||
console.error('Login exception:', e);
|
||
showAlert('loginAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function register() {
|
||
const email = document.getElementById('regEmail').value;
|
||
const password = document.getElementById('regPassword').value;
|
||
const name = document.getElementById('regName').value;
|
||
const phone = document.getElementById('regPhone').value;
|
||
|
||
if (!email || !password || !name) {
|
||
showAlert('registerAlert', '❌ Заполните обязательные поля', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`${API_CONFIG.userService}/api/v1/auth/register`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email,
|
||
password,
|
||
full_name: name,
|
||
phone
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const text = await res.text();
|
||
throw new Error(text.substring(0, 100));
|
||
}
|
||
|
||
showAlert('registerAlert', '✅ Регистрация успешна! Авторизуйтесь.', 'success');
|
||
setTimeout(() => {
|
||
closeModal('registerModal');
|
||
document.getElementById('loginEmail').value = email;
|
||
document.getElementById('loginPassword').value = password;
|
||
}, 1500);
|
||
} catch (e) {
|
||
console.error('Register error:', e);
|
||
showAlert('registerAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
appState.token = null;
|
||
appState.isLoggedIn = false;
|
||
localStorage.removeItem('token');
|
||
showLogin();
|
||
}
|
||
|
||
// UI functions
|
||
function showLogin() {
|
||
document.getElementById('loginPage').style.display = 'flex';
|
||
document.getElementById('appPage').style.display = 'none';
|
||
}
|
||
|
||
function showApp() {
|
||
document.getElementById('loginPage').style.display = 'none';
|
||
document.getElementById('appPage').style.display = 'flex';
|
||
}
|
||
|
||
function navigate(page) {
|
||
try {
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
const targetPage = document.getElementById(page);
|
||
if (targetPage) {
|
||
targetPage.classList.add('active');
|
||
} else {
|
||
console.warn(`Page with id ${page} not found`);
|
||
return;
|
||
}
|
||
|
||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||
if (event && event.target) {
|
||
event.target.classList.add('active');
|
||
}
|
||
|
||
const titles = {
|
||
profile: '📋 Профиль',
|
||
sos: '🚨 SOS Сигнал',
|
||
alerts: '🔔 Мои алерты',
|
||
calendar: '📅 Календарь',
|
||
nutrition: '🥗 Питание',
|
||
contacts: '👥 Контакты',
|
||
settings: '⚙️ Настройки',
|
||
location: '📍 Рядом',
|
||
safety: '🛡️ Безопасные зоны',
|
||
responses: '✅ Ответы'
|
||
};
|
||
|
||
const pageTitle = document.getElementById('pageTitle');
|
||
if (pageTitle) {
|
||
pageTitle.textContent = titles[page] || page;
|
||
}
|
||
|
||
// Load data for specific pages
|
||
if (page === 'alerts') loadAlerts();
|
||
if (page === 'profile') loadUserProfile();
|
||
if (page === 'location') loadNearbyUsers();
|
||
} catch (e) {
|
||
console.error('Navigation error:', e);
|
||
}
|
||
}
|
||
|
||
function showAlert(elementId, message, type) {
|
||
let el = document.getElementById(elementId);
|
||
|
||
if (!el) {
|
||
el = document.createElement('div');
|
||
el.id = elementId;
|
||
const loginForm = document.querySelector('.login-form');
|
||
const modalContent = document.querySelector('.modal-content');
|
||
const parent = loginForm || modalContent;
|
||
|
||
if (!parent) return;
|
||
parent.insertBefore(el, parent.firstChild);
|
||
}
|
||
|
||
el.className = `alert ${type}`;
|
||
el.textContent = message;
|
||
el.style.display = 'block';
|
||
|
||
if (type === 'success') {
|
||
setTimeout(() => el.style.display = 'none', 3000);
|
||
}
|
||
}
|
||
|
||
function closeModal(id) {
|
||
document.getElementById(id).classList.remove('active');
|
||
}
|
||
|
||
// API functions
|
||
async function apiCall(url, method = 'GET', body = null) {
|
||
try {
|
||
const options = {
|
||
method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
};
|
||
|
||
if (appState.token) {
|
||
options.headers['Authorization'] = `Bearer ${appState.token}`;
|
||
}
|
||
|
||
if (body) options.body = JSON.stringify(body);
|
||
|
||
const res = await fetch(url, options);
|
||
|
||
if (res.status === 401) {
|
||
logout();
|
||
throw new Error('Сессия истекла');
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const text = await res.text();
|
||
console.error(`API error ${res.status}:`, text);
|
||
throw new Error(`Ошибка ${res.status}`);
|
||
}
|
||
|
||
const text = await res.text();
|
||
return text ? JSON.parse(text) : {};
|
||
} catch (e) {
|
||
console.error('API call error:', e);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
// Profile functions
|
||
async function loadUserProfile() {
|
||
try {
|
||
const user = await apiCall(`${API_CONFIG.userService}/api/v1/users/me`);
|
||
appState.user = user;
|
||
|
||
const name = user.full_name || user.email || 'Пользователь';
|
||
document.getElementById('userNameDisplay').textContent = name;
|
||
document.getElementById('userEmailDisplay').textContent = user.email || '';
|
||
|
||
const info = `
|
||
<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; line-height: 2;">
|
||
<strong>📧 Email:</strong> ${user.email || 'не указан'}<br>
|
||
<strong>👤 Имя:</strong> ${user.full_name || 'не указано'}<br>
|
||
<strong>📱 Телефон:</strong> ${user.phone || 'не указан'}<br>
|
||
<strong>🏠 Город:</strong> ${user.city || 'не указан'}<br>
|
||
<strong>📍 Статус:</strong> <span class="status-badge status-active">Активна</span>
|
||
</div>
|
||
`;
|
||
document.getElementById('profileInfo').innerHTML = info;
|
||
} catch (e) {
|
||
console.error('Profile load error:', e);
|
||
// Try alternative endpoint
|
||
try {
|
||
const user = await apiCall(`${API_CONFIG.userService}/api/v1/profile`);
|
||
document.getElementById('userNameDisplay').textContent = user.full_name || user.email;
|
||
document.getElementById('userEmailDisplay').textContent = user.email;
|
||
} catch (e2) {
|
||
console.error('Profile alternative endpoint failed:', e2);
|
||
}
|
||
}
|
||
}
|
||
|
||
function showEditProfile() {
|
||
alert('Функция редактирования профиля в разработке');
|
||
}
|
||
|
||
// Alert functions
|
||
async function createSOS() {
|
||
const type = document.getElementById('sosType').value;
|
||
const lat = parseFloat(document.getElementById('sosLat').value);
|
||
const lon = parseFloat(document.getElementById('sosLon').value);
|
||
const address = document.getElementById('sosAddress').value;
|
||
const message = document.getElementById('sosMessage').value;
|
||
|
||
if (!lat || !lon || !address || !message) {
|
||
showAlert('sosAlert', '❌ Заполните все поля', 'error');
|
||
return;
|
||
}
|
||
|
||
if (isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
||
showAlert('sosAlert', '❌ Некорректные координаты', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAlert('sosAlert', '⏳ Отправка SOS...', 'info');
|
||
|
||
const payload = {
|
||
alert_type: type,
|
||
latitude: lat,
|
||
longitude: lon,
|
||
address: address,
|
||
message: message,
|
||
contact_emergency_services: true,
|
||
notify_emergency_contacts: true
|
||
};
|
||
|
||
console.log('Creating SOS with payload:', payload);
|
||
|
||
const res = await apiCall(`${API_CONFIG.emergencyService}/api/v1/alert`, 'POST', payload);
|
||
|
||
console.log('SOS response:', res);
|
||
|
||
showAlert('sosAlert', '✅ SOS сигнал отправлен!', 'success');
|
||
setTimeout(() => {
|
||
document.getElementById('sosMessage').value = 'Требуется немедленная помощь';
|
||
navigate('alerts');
|
||
loadAlerts();
|
||
}, 1500);
|
||
} catch (e) {
|
||
console.error('SOS error:', e);
|
||
showAlert('sosAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Получить текущую геолокацию
|
||
function getCurrentLocation() {
|
||
return new Promise((resolve, reject) => {
|
||
if (navigator.geolocation) {
|
||
navigator.geolocation.getCurrentPosition(
|
||
(position) => {
|
||
resolve({
|
||
lat: position.coords.latitude,
|
||
lon: position.coords.longitude
|
||
});
|
||
},
|
||
(error) => {
|
||
console.error('Geolocation error:', error);
|
||
reject(new Error('Не удалось получить вашу геолокацию. Проверьте разрешения браузера.'));
|
||
}
|
||
);
|
||
} else {
|
||
reject(new Error('Геолокация не поддерживается вашим браузером'));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Получить алерты рядом (в радиусе 1км)
|
||
async function loadAlertsNearby() {
|
||
try {
|
||
showAlert('nearbyAlertsInfo', '⏳ Получение вашей геопозиции...', 'info');
|
||
document.getElementById('nearbyAlertsInfo').style.display = 'block';
|
||
|
||
const location = await getCurrentLocation();
|
||
console.log('User location:', location);
|
||
|
||
showAlert('nearbyAlertsInfo', '⏳ Поиск алертов рядом с вами...', 'info');
|
||
|
||
const res = await apiCall(
|
||
`${API_CONFIG.emergencyService}/api/v1/alerts/nearby?latitude=${location.lat}&longitude=${location.lon}&radius_km=1`,
|
||
'GET'
|
||
);
|
||
|
||
const alerts = Array.isArray(res) ? res : (res?.alerts || res?.data || []);
|
||
console.log('Nearby alerts:', alerts);
|
||
|
||
let html = '';
|
||
if (alerts.length > 0) {
|
||
html = `<strong>🎯 Найдено алертов рядом: ${alerts.length}</strong><br><br>`;
|
||
html += alerts.map(alert => {
|
||
const distance = alert.distance_km ? `(${alert.distance_km} км)` : '';
|
||
return `
|
||
<div style="padding: 10px; margin-bottom: 10px; background: white; border-left: 4px solid #ff9800; border-radius: 3px;">
|
||
<strong>${alert.alert_type}</strong> ${distance}<br>
|
||
<div style="font-size: 0.9em; color: #666;">${alert.address || 'Адрес не указан'}</div>
|
||
<div style="font-size: 0.85em; color: #999; margin-top: 3px;">
|
||
${new Date(alert.created_at).toLocaleString('ru-RU')}
|
||
</div>
|
||
<div style="margin-top: 5px; font-size: 0.85em;">
|
||
👥 ${alert.responded_users_count || 0} человек откликнулись
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else {
|
||
html = '<strong>✅ Алертов рядом не найдено</strong>';
|
||
}
|
||
|
||
const nearbyList = document.getElementById('nearbyAlertsList');
|
||
if (nearbyList) {
|
||
nearbyList.innerHTML = html;
|
||
}
|
||
showAlert('nearbyAlertsInfo', `📍 Ваша позиция: ${location.lat.toFixed(4)}, ${location.lon.toFixed(4)}`, 'success');
|
||
} catch (e) {
|
||
console.error('Error loading nearby alerts:', e);
|
||
showAlert('nearbyAlertsInfo', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function loadAlerts() {
|
||
try {
|
||
console.log('Loading alerts from:', `${API_CONFIG.emergencyService}/api/v1/alerts/my`);
|
||
|
||
const res = await apiCall(`${API_CONFIG.emergencyService}/api/v1/alerts/my`, 'GET');
|
||
const alerts = Array.isArray(res) ? res : (res?.alerts || res?.data || []);
|
||
|
||
console.log('Alerts data:', alerts);
|
||
|
||
const html = alerts.length > 0 ? alerts.map(alert => `
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">${alert.alert_type} - ${alert.address}</div>
|
||
<div class="list-item-desc">${alert.message || 'Нет описания'}</div>
|
||
<div style="margin-top: 5px; font-size: 0.85em; color: #999;">
|
||
${new Date(alert.created_at || new Date()).toLocaleString('ru-RU')}
|
||
</div>
|
||
<div style="margin-top: 5px;">
|
||
<span class="status-badge ${alert.is_resolved ? 'status-resolved' : 'status-active'}">
|
||
${alert.is_resolved ? '✅ Разрешено' : '🔴 Активно'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
`).join('') : '<li class="list-item"><div class="list-item-content"><div class="list-item-title">Нет алертов</div></div></li>';
|
||
|
||
const alertsList = document.getElementById('alertsList');
|
||
if (alertsList) alertsList.innerHTML = html;
|
||
|
||
const totalAlerts = document.getElementById('totalAlerts');
|
||
if (totalAlerts) totalAlerts.textContent = alerts.length;
|
||
|
||
const activeAlerts = document.getElementById('activeAlerts');
|
||
if (activeAlerts) activeAlerts.textContent = alerts.filter(a => !a.is_resolved).length;
|
||
} catch (e) {
|
||
console.error('Alerts load error:', e);
|
||
const alertsList = document.getElementById('alertsList');
|
||
if (alertsList) {
|
||
alertsList.innerHTML = '<li class="list-item"><div class="list-item-content"><div class="list-item-title">Ошибка загрузки: ' + e.message + '</div></div></li>';
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadNearbyUsers() {
|
||
try {
|
||
// Get user's current location
|
||
const userLat = parseFloat(document.getElementById('sosLat')?.value) || 55.7558; // Moscow default
|
||
const userLon = parseFloat(document.getElementById('sosLon')?.value) || 37.6173;
|
||
|
||
console.log('Loading nearby users from:', `${API_CONFIG.locationService}/api/v1/nearby-users?latitude=${userLat}&longitude=${userLon}`);
|
||
|
||
const users = await apiCall(`${API_CONFIG.locationService}/api/v1/nearby-users?latitude=${userLat}&longitude=${userLon}`, 'GET');
|
||
console.log('Nearby users data:', users);
|
||
|
||
const html = (users || []).map(user => `
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">${user.full_name || user.name || 'Анонимно'}</div>
|
||
<div class="list-item-desc">📍 ${user.distance || user.distance_km || '?'} км</div>
|
||
</div>
|
||
</li>
|
||
`).join('');
|
||
|
||
const nearbyList = document.getElementById('nearbyList');
|
||
if (nearbyList) {
|
||
nearbyList.innerHTML = html || '<li class="list-item"><div class="list-item-content"><div class="list-item-title">Никого рядом</div></div></li>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Nearby users load error:', e);
|
||
const nearbyList = document.getElementById('nearbyList');
|
||
if (nearbyList) {
|
||
nearbyList.innerHTML = '<li class="list-item"><div class="list-item-content"><div class="list-item-title">Ошибка: ' + e.message + '</div></div></li>';
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadContacts() {
|
||
try {
|
||
console.log('Loading contacts from:', `${API_CONFIG.userService}/api/v1/users/me/emergency-contacts`);
|
||
|
||
const data = await apiCall(`${API_CONFIG.userService}/api/v1/users/me/emergency-contacts`, 'GET');
|
||
|
||
console.log('Contacts data:', data);
|
||
|
||
const contacts = Array.isArray(data) ? data : (data?.contacts || data?.data || []);
|
||
const container = document.getElementById('contactsList');
|
||
|
||
if (!container) {
|
||
console.warn('contactsList container not found');
|
||
return;
|
||
}
|
||
|
||
if (!contacts || contacts.length === 0) {
|
||
container.innerHTML = '<p style="color: #999;">Контакты не добавлены</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = contacts.map(c => `
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">${c.name}</div>
|
||
<div class="list-item-desc">📱 ${c.phone_number || c.phone}</div>
|
||
<div class="list-item-desc"><small>${c.relation}</small></div>
|
||
</div>
|
||
</li>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Load contacts error:', e);
|
||
const container = document.getElementById('contactsList');
|
||
if (container) {
|
||
container.innerHTML = `<p style="color: red;">Ошибка загрузки: ${e.message}</p>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadCalendarEntries() {
|
||
try {
|
||
console.log('Loading calendar entries from:', `${API_CONFIG.calendarService}/api/v1/calendar/entries`);
|
||
|
||
const data = await apiCall(`${API_CONFIG.calendarService}/api/v1/calendar/entries`, 'GET');
|
||
|
||
console.log('Calendar data:', data);
|
||
|
||
const entries = Array.isArray(data) ? data : (data?.entries || data?.data || []);
|
||
const container = document.getElementById('calendarList');
|
||
|
||
if (!container) {
|
||
console.warn('calendarList container not found');
|
||
return;
|
||
}
|
||
|
||
if (!entries || entries.length === 0) {
|
||
container.innerHTML = '<li class="list-item"><div class="list-item-content"><div class="list-item-title">Записей нет</div></div></li>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = entries.map(e => {
|
||
const entryType = e.entry_type || e.type || 'mood';
|
||
const moodDisplay = e.mood ? `😊 ${e.mood}` : '—';
|
||
const dateDisplay = e.entry_date || e.date || 'неизвестно';
|
||
const notesDisplay = e.notes || e.period_symptoms || 'нет заметок';
|
||
|
||
return `
|
||
<li class="list-item">
|
||
<div class="list-item-content">
|
||
<div class="list-item-title">
|
||
📅 ${entryType} ${moodDisplay}
|
||
</div>
|
||
<div class="list-item-desc">${dateDisplay}</div>
|
||
<div class="list-item-desc"><small>${notesDisplay}</small></div>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}).join('');
|
||
} catch (e) {
|
||
console.error('Load calendar error:', e);
|
||
const container = document.getElementById('calendarList');
|
||
if (container) {
|
||
container.innerHTML = `<li class="list-item"><div class="list-item-content"><div class="list-item-title">Ошибка: ${e.message}</div></div></li>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Calendar functions
|
||
async function addCalendarEntry() {
|
||
const date = document.getElementById('calendarDate').value;
|
||
const type = document.getElementById('calendarType').value;
|
||
const mood = document.getElementById('calendarMood').value;
|
||
const notes = document.getElementById('calendarNotes').value;
|
||
|
||
if (!date) {
|
||
showAlert('calendarAlert', '❌ Выберите дату', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAlert('calendarAlert', '⏳ Добавление записи...', 'info');
|
||
|
||
// Конвертируем mood значение в правильный формат
|
||
const moodMap = {
|
||
'happy': 'happy',
|
||
'sad': 'sad',
|
||
'anxious': 'anxious',
|
||
'irritated': 'irritated'
|
||
};
|
||
const mappedMood = moodMap[mood] || null;
|
||
|
||
const payload = {
|
||
entry_date: date,
|
||
entry_type: type || 'mood',
|
||
mood: mappedMood,
|
||
notes: notes || '',
|
||
period_symptoms: null,
|
||
flow_intensity: null,
|
||
energy_level: null,
|
||
sleep_hours: null,
|
||
symptoms: null,
|
||
medications: null
|
||
};
|
||
|
||
console.log('Adding calendar entry:', payload);
|
||
|
||
const res = await apiCall(`${API_CONFIG.calendarService}/api/v1/calendar/entries`, 'POST', payload);
|
||
|
||
console.log('Calendar response:', res);
|
||
|
||
showAlert('calendarAlert', '✅ Запись добавлена!', 'success');
|
||
document.getElementById('calendarNotes').value = '';
|
||
setTimeout(() => loadCalendarEntries(), 500);
|
||
} catch (e) {
|
||
console.error('Calendar error:', e);
|
||
showAlert('calendarAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Nutrition functions
|
||
async function addNutrition() {
|
||
const date = document.getElementById('nutritionDate').value;
|
||
const dish = document.getElementById('nutritionDish').value;
|
||
const desc = document.getElementById('nutritionDesc').value;
|
||
|
||
if (!date || !dish) {
|
||
showAlert('nutritionAlert', '❌ Заполните обязательные поля', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAlert('nutritionAlert', '⏳ Добавление записи...', 'info');
|
||
|
||
const payload = {
|
||
entry_date: date,
|
||
meal_type: 'breakfast',
|
||
custom_food_name: dish,
|
||
quantity: 1,
|
||
unit: 'portion',
|
||
calories: 0,
|
||
notes: desc
|
||
};
|
||
|
||
console.log('Adding nutrition entry:', payload);
|
||
|
||
await apiCall(`${API_CONFIG.nutritionService}/api/v1/nutrition/entries`, 'POST', payload);
|
||
|
||
showAlert('nutritionAlert', '✅ Блюдо добавлено!', 'success');
|
||
document.getElementById('nutritionDish').value = '';
|
||
document.getElementById('nutritionDesc').value = '';
|
||
} catch (e) {
|
||
console.error('Nutrition error:', e);
|
||
showAlert('nutritionAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Contact functions
|
||
async function addContact() {
|
||
const name = document.getElementById('contactName').value;
|
||
const phone = document.getElementById('contactPhone').value;
|
||
const relation = document.getElementById('contactRelation').value;
|
||
|
||
if (!name || !phone) {
|
||
showAlert('contactsAlert', '❌ Заполните обязательные поля', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showAlert('contactsAlert', '⏳ Добавление контакта...', 'info');
|
||
|
||
const payload = {
|
||
name: name,
|
||
phone_number: phone,
|
||
relation: relation || 'Emergency Contact'
|
||
};
|
||
|
||
console.log('Adding contact with payload:', payload);
|
||
|
||
await apiCall(`${API_CONFIG.userService}/api/v1/users/me/emergency-contacts`, 'POST', payload);
|
||
|
||
showAlert('contactsAlert', '✅ Контакт добавлен!', 'success');
|
||
document.getElementById('contactName').value = '';
|
||
document.getElementById('contactPhone').value = '';
|
||
document.getElementById('contactRelation').value = '';
|
||
setTimeout(() => loadContacts(), 500);
|
||
} catch (e) {
|
||
console.error('Add contact error:', e);
|
||
showAlert('contactsAlert', '❌ ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Modal functions
|
||
function showRegister() {
|
||
document.getElementById('registerModal').classList.add('active');
|
||
}
|
||
|
||
// Utility functions
|
||
function saveSettings() {
|
||
showAlert('settingsAlert', '✅ Настройки сохранены!', 'success');
|
||
}
|
||
|
||
// Set default date to today
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('calendarDate').value = today;
|
||
document.getElementById('nutritionDate').value = today;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|