user profile

This commit is contained in:
root
2026-01-13 07:11:21 +03:00
parent 24ea8deeec
commit 3d39c1ba07
12 changed files with 1312 additions and 47 deletions
+266
View File
@@ -0,0 +1,266 @@
{% extends 'layouts/base.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block styles %}
<style>
.profile-header {
text-align: center;
padding: 2rem 0;
}
.avatar-container {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto 1rem;
}
.avatar-img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.avatar-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.avatar-upload-btn {
position: absolute;
bottom: 0;
right: 0;
width: 36px;
height: 36px;
border-radius: 50%;
background: #0d6efd;
color: white;
border: 3px solid #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
z-index: 10;
}
.avatar-upload-btn:hover {
background: #0b5ed7;
transform: scale(1.1);
}
.nav-pills .nav-link {
color: #495057;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
margin-bottom: 0.5rem;
}
.nav-pills .nav-link.active {
background-color: #0d6efd;
color: white;
}
.nav-pills .nav-link:hover:not(.active) {
background-color: #e9ecef;
}
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.btn-save {
min-width: 150px;
}
#avatarPreviewModal {
max-width: 200px;
max-height: 200px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-3">
<!-- Боковая панель навигации -->
<div class="card mb-4">
<div class="card-body">
<div class="profile-header">
<div class="avatar-container">
{% if user.avatar %}
<img src="{{ base_url('/uploads/avatars/' ~ user.avatar) }}" alt="Аватар" class="avatar-img">
{% else %}
<div class="avatar-placeholder">
{{ user.name|first|upper }}
</div>
{% endif %}
<label for="avatarInput" class="avatar-upload-btn" title="Изменить аватар">
<i class="fa-solid fa-camera"></i>
</label>
<input type="file" id="avatarInput" name="avatar" accept="image/jpeg,image/png,image/gif" style="display: none;">
</div>
<h5 class="mb-1">{{ user.name }}</h5>
<p class="text-muted mb-0">{{ user.email }}</p>
</div>
<hr>
<nav class="nav-pills flex-column">
<a href="{{ base_url('/profile') }}" class="nav-link {{ active_tab == 'general' ? 'active' : '' }}">
<i class="fa-solid fa-user me-2"></i> Основное
</a>
<a href="{{ base_url('/profile/organizations') }}" class="nav-link {{ active_tab == 'organizations' ? 'active' : '' }}">
<i class="fa-solid fa-building me-2"></i> Мои организации
</a>
<a href="{{ base_url('/profile/security') }}" class="nav-link {{ active_tab == 'security' ? 'active' : '' }}">
<i class="fa-solid fa-shield-halved me-2"></i> Безопасность
</a>
</nav>
</div>
</div>
</div>
<div class="col-md-9">
<!-- Основной контент -->
<div class="card">
<div class="card-header bg-white">
<h4 class="mb-0">Основная информация</h4>
</div>
<div class="card-body">
<form action="{{ base_url('/profile/update-name') }}" method="post">
{{ csrf_field()|raw }}
<div class="mb-3">
<label for="name" class="form-label">Имя</label>
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}" required minlength="3">
<div class="form-text">Ваше имя будет отображаться в системе</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" value="{{ user.email }}" disabled>
<div class="form-text">Email нельзя изменить (он является вашим логином)</div>
</div>
<div class="mb-3">
<label for="created_at" class="form-label">Дата регистрации</label>
<input type="text" class="form-control" id="created_at" value="{{ user.created_at|date('d.m.Y H:i') }}" disabled>
</div>
<button type="submit" class="btn btn-primary btn-save">
<i class="fa-solid fa-check me-1"></i> Сохранить
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Модальное окно загрузки аватара -->
<div class="modal fade" id="avatarModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Загрузка аватара</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{{ base_url('/profile/upload-avatar') }}" method="post" enctype="multipart/form-data" id="avatarForm">
{{ csrf_field()|raw }}
<div class="modal-body text-center">
<p class="text-muted mb-3">Выберите изображение JPG, PNG или GIF (максимум 2 МБ)</p>
<div id="avatarPreviewContainer" style="display: none; margin-bottom: 15px;">
<img id="avatarPreviewModal" src="" alt="Превью аватара">
</div>
<input type="file" class="form-control" id="avatarFile" name="avatar" accept="image/jpeg,image/png,image/gif" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-upload me-1"></i> Загрузить
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const avatarInput = document.getElementById('avatarInput');
const avatarFile = document.getElementById('avatarFile');
const avatarModal = new bootstrap.Modal(document.getElementById('avatarModal'));
const avatarPreviewModal = document.getElementById('avatarPreviewModal');
const avatarPreviewContainer = document.getElementById('avatarPreviewContainer');
// При клике на кнопку камеры - открываем модалку
avatarInput.addEventListener('click', function(e) {
e.preventDefault();
avatarModal.show();
});
// При изменении файла в модалке - показываем превью
avatarFile.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) {
avatarPreviewContainer.style.display = 'none';
return;
}
// Проверка типа файла
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert('Разрешены только файлы JPG, PNG и GIF');
this.value = '';
avatarPreviewContainer.style.display = 'none';
return;
}
// Проверка размера (2 МБ)
if (file.size > 2 * 1024 * 1024) {
alert('Максимальный размер файла - 2 МБ');
this.value = '';
avatarPreviewContainer.style.display = 'none';
return;
}
// Показываем превью
const reader = new FileReader();
reader.onload = function(event) {
avatarPreviewModal.src = event.target.result;
avatarPreviewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
});
// Очистка при закрытии модалки
avatarModal._element.addEventListener('hidden.bs.modal', function() {
avatarFile.value = '';
avatarPreviewContainer.style.display = 'none';
avatarPreviewModal.src = '';
});
});
</script>
{% endblock %}
+368
View File
@@ -0,0 +1,368 @@
{% extends 'layouts/base.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block styles %}
<style>
.profile-header {
text-align: center;
padding: 2rem 0;
}
.avatar-container {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto 1rem;
}
.avatar-img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.avatar-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.nav-pills .nav-link {
color: #495057;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
margin-bottom: 0.5rem;
}
.nav-pills .nav-link.active {
background-color: #0d6efd;
color: white;
}
.nav-pills .nav-link:hover:not(.active) {
background-color: #e9ecef;
}
.org-current-badge {
background-color: #198754;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
margin-left: 8px;
text-transform: uppercase;
}
</style>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-3">
<!-- Боковая панель навигации -->
<div class="card mb-4">
<div class="card-body">
<div class="profile-header">
<div class="avatar-container">
{% if user.avatar %}
<img src="{{ base_url('/uploads/avatars/' ~ user.avatar) }}" alt="Аватар" class="avatar-img">
{% else %}
<div class="avatar-placeholder">
{{ user.name|first|upper }}
</div>
{% endif %}
</div>
<h5 class="mb-1">{{ user.name }}</h5>
<p class="text-muted mb-0">{{ user.email }}</p>
</div>
<hr>
<nav class="nav-pills flex-column">
<a href="{{ base_url('/profile') }}" class="nav-link {{ active_tab == 'general' ? 'active' : '' }}">
<i class="fa-solid fa-user me-2"></i> Основное
</a>
<a href="{{ base_url('/profile/organizations') }}" class="nav-link {{ active_tab == 'organizations' ? 'active' : '' }}">
<i class="fa-solid fa-building me-2"></i> Мои организации
</a>
<a href="{{ base_url('/profile/security') }}" class="nav-link {{ active_tab == 'security' ? 'active' : '' }}">
<i class="fa-solid fa-shield-halved me-2"></i> Безопасность
</a>
</nav>
</div>
</div>
</div>
<div class="col-md-9">
<!-- Список организаций -->
<div class="card">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">Мои организации</h4>
<a href="{{ base_url('/organizations/create') }}" class="btn btn-primary btn-sm">
<i class="fa-solid fa-plus me-1"></i> Создать организацию
</a>
</div>
<div class="card-body">
{% if organizations is empty %}
<div class="text-center py-5">
<i class="fa-solid fa-building text-muted mb-3" style="font-size: 48px;"></i>
<p class="text-muted mb-3">У вас пока нет организаций</p>
<a href="{{ base_url('/organizations/create') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-1"></i> Создать первую организацию
</a>
</div>
{% else %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Организация</th>
<th>Тип</th>
<th>Ваша роль</th>
<th>Дата входа</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for org in organizations %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-primary text-white rounded me-3 d-flex align-items-center justify-content-center" style="width:40px; height:40px;">
<i class="fa-solid fa-building"></i>
</div>
<div>
<strong>{{ org.name }}</strong>
{% if org.is_current_org %}
<span class="org-current-badge">Текущая</span>
{% endif %}
<br>
<small class="text-muted">ID: {{ org.id }}</small>
</div>
</div>
</td>
<td>
{% if org.type == 'personal' %}
<span class="badge bg-info">Личное пространство</span>
{% else %}
<span class="badge bg-success">Бизнес</span>
{% endif %}
</td>
<td>
{% if org.role == 'owner' %}
<span class="badge bg-warning text-dark">Владелец</span>
{% elseif org.role == 'admin' %}
<span class="badge bg-primary">Администратор</span>
{% elseif org.role == 'manager' %}
<span class="badge bg-secondary">Менеджер</span>
{% else %}
<span class="badge bg-light text-dark">Гость</span>
{% endif %}
</td>
<td>{{ org.joined_at ? org.joined_at|date('d.m.Y H:i') : '—' }}</td>
<td>
<div class="btn-group btn-group-sm">
{# ВЛАДЕЛЕЦ #}
{% if org.is_owner %}
{# Текущая организация #}
{% if org.is_current_org %}
<a href="{{ base_url('/organizations/' ~ org.id ~ '/dashboard') }}" class="btn btn-outline-primary" title="Дашборд">
<i class="fa-solid fa-gauge-high"></i>
</a>
<a href="{{ base_url('/organizations/' ~ org.id ~ '/edit') }}" class="btn btn-outline-secondary" title="Редактировать">
<i class="fa-solid fa-pen"></i>
</a>
<button type="button" class="btn btn-outline-danger delete-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Удалить">
<i class="fa-solid fa-trash"></i>
</button>
{% else %}
{# Не текущая организация #}
<a href="{{ base_url('/organizations/switch/' ~ org.id) }}" class="btn btn-outline-success" title="Выбрать">
<i class="fa-solid fa-check"></i>
</a>
<a href="{{ base_url('/organizations/' ~ org.id ~ '/edit') }}" class="btn btn-outline-secondary" title="Редактировать">
<i class="fa-solid fa-pen"></i>
</a>
<button type="button" class="btn btn-outline-danger delete-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Удалить">
<i class="fa-solid fa-trash"></i>
</button>
{% endif %}
{% elseif org.role in ['admin', 'manager'] %}
{# Админ/Менеджер #}
{% if org.is_current_org %}
<a href="{{ base_url('/organizations/' ~ org.id ~ '/dashboard') }}" class="btn btn-outline-primary" title="Дашборд">
<i class="fa-solid fa-gauge-high"></i>
</a>
<button type="button" class="btn btn-outline-danger leave-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Покинуть">
<i class="fa-solid fa-person-walking-arrow-right"></i>
</button>
{% else %}
<a href="{{ base_url('/organizations/switch/' ~ org.id) }}" class="btn btn-outline-success" title="Выбрать">
<i class="fa-solid fa-check"></i>
</a>
<button type="button" class="btn btn-outline-danger leave-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Покинуть">
<i class="fa-solid fa-person-walking-arrow-right"></i>
</button>
{% endif %}
{% else %}
{# Гость (не owner, не admin, не manager) #}
{% if org.is_current_org %}
<button type="button" class="btn btn-outline-danger leave-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Покинуть">
<i class="fa-solid fa-person-walking-arrow-right"></i>
</button>
{% else %}
<a href="{{ base_url('/organizations/switch/' ~ org.id) }}" class="btn btn-outline-success" title="Выбрать">
<i class="fa-solid fa-check"></i>
</a>
<button type="button" class="btn btn-outline-danger leave-org-btn"
data-org-id="{{ org.id }}"
data-org-name="{{ org.name }}"
title="Покинуть">
<i class="fa-solid fa-person-walking-arrow-right"></i>
</button>
{% endif %}
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения выхода из организации -->
<div class="modal fade" id="leaveOrgModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Покинуть организацию</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Вы уверены, что хотите покинуть организацию <strong id="leaveOrgName"></strong>?</p>
<p class="text-muted mb-0">После выхода вы потеряете доступ к данным этой организации.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" id="confirmLeaveOrg">
<i class="fa-solid fa-person-walking-arrow-right me-1"></i> Покинуть
</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения удаления организации -->
<div class="modal fade" id="deleteOrgModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Удалить организацию</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-danger"><i class="fa-solid fa-triangle-exclamation me-2"></i>Внимание! Это действие нельзя отменить.</p>
<p>Вы уверены, что хотите удалить организацию <strong id="deleteOrgName"></strong>?</p>
<p class="text-muted mb-0">Все данные организации и её участники будут удалены.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<a href="#" class="btn btn-danger" id="confirmDeleteOrg">
<i class="fa-solid fa-trash me-1"></i> Удалить
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const leaveOrgModal = new bootstrap.Modal(document.getElementById('leaveOrgModal'));
const deleteOrgModal = new bootstrap.Modal(document.getElementById('deleteOrgModal'));
let selectedOrgId = null;
// Обработчики кнопок выхода из организации
document.querySelectorAll('.leave-org-btn').forEach(btn => {
btn.addEventListener('click', function() {
selectedOrgId = this.dataset.orgId;
const orgName = this.dataset.orgName;
document.getElementById('leaveOrgName').textContent = orgName;
leaveOrgModal.show();
});
});
// Обработчики кнопок удаления организации
document.querySelectorAll('.delete-org-btn').forEach(btn => {
btn.addEventListener('click', function() {
const orgId = this.dataset.orgId;
const orgName = this.dataset.orgName;
document.getElementById('deleteOrgName').textContent = orgName;
document.getElementById('confirmDeleteOrg').href = '{{ base_url("/organizations/") }}' + orgId + '/delete';
deleteOrgModal.show();
});
});
// Подтверждение выхода
document.getElementById('confirmLeaveOrg').addEventListener('click', function() {
if (!selectedOrgId) return;
const formData = new FormData();
fetch('{{ base_url("/organizations/") }}' + selectedOrgId + '/leave', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
leaveOrgModal.hide();
if (data.success) {
alert(data.message);
window.location.reload();
} else {
alert(data.message);
}
})
.catch(error => {
leaveOrgModal.hide();
alert('Ошибка при выходе из организации');
});
});
});
</script>
{% endblock %}
+157
View File
@@ -0,0 +1,157 @@
{% extends 'layouts/base.twig' %}
{% block title %}{{ title }}{% endblock %}
{% block styles %}
<style>
.profile-header {
text-align: center;
padding: 2rem 0;
}
.avatar-container {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto 1rem;
}
.avatar-img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.avatar-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
border: 4px solid #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.nav-pills .nav-link {
color: #495057;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
margin-bottom: 0.5rem;
}
.nav-pills .nav-link.active {
background-color: #0d6efd;
color: white;
}
.nav-pills .nav-link:hover:not(.active) {
background-color: #e9ecef;
}
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-3">
<!-- Боковая панель навигации -->
<div class="card mb-4">
<div class="card-body">
<div class="profile-header">
<div class="avatar-container">
{% if user.avatar %}
<img src="{{ base_url('/uploads/avatars/' ~ user.avatar) }}" alt="Аватар" class="avatar-img">
{% else %}
<div class="avatar-placeholder">
{{ user.name|first|upper }}
</div>
{% endif %}
</div>
<h5 class="mb-1">{{ user.name }}</h5>
<p class="text-muted mb-0">{{ user.email }}</p>
</div>
<hr>
<nav class="nav-pills flex-column">
<a href="{{ base_url('/profile') }}" class="nav-link {{ active_tab == 'general' ? 'active' : '' }}">
<i class="fa-solid fa-user me-2"></i> Основное
</a>
<a href="{{ base_url('/profile/organizations') }}" class="nav-link {{ active_tab == 'organizations' ? 'active' : '' }}">
<i class="fa-solid fa-building me-2"></i> Мои организации
</a>
<a href="{{ base_url('/profile/security') }}" class="nav-link {{ active_tab == 'security' ? 'active' : '' }}">
<i class="fa-solid fa-shield-halved me-2"></i> Безопасность
</a>
</nav>
</div>
</div>
</div>
<div class="col-md-9">
<!-- Смена пароля -->
<div class="card mb-4">
<div class="card-header bg-white">
<h4 class="mb-0"><i class="fa-solid fa-key me-2"></i>Смена пароля</h4>
</div>
<div class="card-body">
<form action="{{ base_url('/profile/change-password') }}" method="post">
{{ csrf_field()|raw }}
<div class="mb-3">
<label for="current_password" class="form-label">Текущий пароль</label>
<input type="password" class="form-control" id="current_password" name="current_password" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">Новый пароль</label>
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
<div class="form-text">Минимум 6 символов</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Подтвердите новый пароль</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<div class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
После смены пароля вы будете автоматически разлогинены на всех устройствах для безопасности.
</div>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-check me-1"></i> Изменить пароль
</button>
</form>
</div>
</div>
<!-- Информация о безопасности -->
<div class="card">
<div class="card-header bg-white">
<h4 class="mb-0"><i class="fa-solid fa-shield-halved me-2"></i>Рекомендации по безопасности</h4>
</div>
<div class="card-body">
<ul class="mb-0">
<li class="mb-2">Используйте пароль длиной не менее 8 символов</li>
<li class="mb-2">Комбинируйте буквы, цифры и специальные символы</li>
<li class="mb-2">Не используйте один и тот же пароль для разных сервисов</li>
<li class="mb-2">Регулярно меняйте пароль</li>
<li>Не сообщайте пароль третьим лицам</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}