Files
chat/app.html
Andrew K. Choi cfc93cb99a
All checks were successful
continuous-integration/drone/push Build is passing
feat: Fix nutrition service and add location-based alerts
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
2025-12-13 16:34:50 +09:00

1592 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>