Примеры админ-панели
Готовые примеры создания административной панели с использованием MNRFY Framework
Базовый макет админ-панели
layouts/admin.html
<!DOCTYPE html>
<html lang="{{ locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ t('admin.panel') }}{% endblock %} - {{ config('app.name') }}</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
{% block styles %}
<link rel="stylesheet" href="{{ asset('css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ asset('css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ asset('css/admin.css') }}">
{% endblock %}
</head>
<body class="admin-layout">
<!-- Верхняя панель -->
<nav class="admin-navbar">
<div class="navbar-brand">
<i class="fas fa-shield-alt"></i>
{{ t('admin.panel') }}
</div>
<div class="navbar-menu">
<a href="{{ route('home') }}" target="_blank" class="navbar-item">
<i class="fas fa-external-link-alt"></i>
{{ t('admin.view_site') }}
</a>
<div class="navbar-user dropdown">
<button class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ auth().avatar ?? asset('images/default-avatar.png') }}"
alt="{{ auth().name }}" class="user-avatar">
{{ auth().name }}
<i class="fas fa-chevron-down"></i>
</button>
<div class="dropdown-menu">
<a href="{{ route('profile') }}" class="dropdown-item">
<i class="fas fa-user"></i> {{ t('menu.profile') }}
</a>
<a href="{{ route('settings') }}" class="dropdown-item">
<i class="fas fa-cog"></i> {{ t('menu.settings') }}
</a>
<div class="dropdown-divider"></div>
<a href="{{ route('logout') }}" class="dropdown-item">
<i class="fas fa-sign-out-alt"></i> {{ t('menu.logout') }}
</a>
</div>
</div>
</div>
</nav>
<div class="admin-wrapper">
<!-- Боковая панель -->
<aside class="admin-sidebar">
{% block sidebar %}
<div class="sidebar-menu">
<a href="{{ route('admin.dashboard') }}"
class="menu-item {{ is_current_route('admin.dashboard') ? 'active' : '' }}">
<i class="fas fa-tachometer-alt"></i>
{{ t('admin.dashboard') }}
</a>
<div class="menu-section">
<h4>{{ t('admin.content_management') }}</h4>
<a href="{{ route('admin.users') }}"
class="menu-item {{ is_current_route('admin.users') ? 'active' : '' }}">
<i class="fas fa-users"></i>
{{ t('admin.users') }}
<span class="badge">{{ db('1752665380840')->countItems('users', {'ban': 0}) }}</span>
</a>
<a href="{{ route('admin.posts') }}"
class="menu-item {{ is_current_route('admin.posts') ? 'active' : '' }}">
<i class="fas fa-newspaper"></i>
{{ t('admin.posts') }}
</a>
<a href="{{ route('admin.comments') }}"
class="menu-item {{ is_current_route('admin.comments') ? 'active' : '' }}">
<i class="fas fa-comments"></i>
{{ t('admin.comments') }}
{% set pendingComments = db('1752665380840')->countItems('comments', {'status': 'pending'}) %}
{% if pendingComments > 0 %}
<span class="badge badge-warning">{{ pendingComments }}</span>
{% endif %}
</a>
</div>
<div class="menu-section">
<h4>{{ t('admin.system') }}</h4>
<a href="{{ route('admin.settings') }}"
class="menu-item {{ is_current_route('admin.settings') ? 'active' : '' }}">
<i class="fas fa-cogs"></i>
{{ t('admin.settings') }}
</a>
<a href="{{ route('admin.logs') }}"
class="menu-item {{ is_current_route('admin.logs') ? 'active' : '' }}">
<i class="fas fa-list"></i>
{{ t('admin.logs') }}
</a>
</div>
</div>
{% endblock %}
</aside>
<!-- Основной контент -->
<main class="admin-content">
<!-- Хлебные крошки -->
{% block breadcrumbs %}
<nav class="breadcrumb-nav">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('admin.dashboard') }}">{{ t('admin.dashboard') }}</a>
</li>
{% block breadcrumb_items %}{% endblock %}
</ol>
</nav>
{% endblock %}
<!-- Заголовок страницы -->
{% block page_header %}
<div class="page-header">
<div class="page-title">
<h1>{% block page_title %}{{ t('admin.panel') }}{% endblock %}</h1>
{% block page_subtitle %}{% endblock %}
</div>
<div class="page-actions">
{% block page_actions %}{% endblock %}
</div>
</div>
{% endblock %}
<!-- Уведомления -->
{% if flash('success') %}
<div class="alert alert-success alert-dismissible">
<i class="fas fa-check-circle"></i>
{{ flash('success') }}
<button type="button" class="btn-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
{% endif %}
{% if flash('error') %}
<div class="alert alert-danger alert-dismissible">
<i class="fas fa-exclamation-circle"></i>
{{ flash('error') }}
<button type="button" class="btn-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
{% endif %}
<!-- Основное содержимое -->
<div class="page-content">
{% block content %}{% endblock %}
</div>
</main>
</div>
{% block scripts %}
<script src="{{ asset('js/jquery.min.js') }}"></script>
<script src="{{ asset('js/bootstrap.min.js') }}"></script>
<script src="{{ asset('js/admin.js') }}"></script>
{% endblock %}
{% block page_scripts %}{% endblock %}
</body>
</html>
Дашборд админ-панели
admin-dashboard.html
{% extends 'layouts/admin.html' %}
{% block title %}{{ t('admin.dashboard') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="dashboard-container">
<!-- Статистические карточки -->
<div class="stats-grid">
{% set totalUsers = db('1752665380840')->countItems('users') %}
{% set activeUsers = db('1752665380840')->countItems('users', {'ban': 0}) %}
{% set onlineUsers = db('1752665380840')->countItems('users', {'last_seen': '>' ~ (time() - 300)}) %}
{% set todayRegistrations = db('1752665380840')->countItems('users', {'created_at': '>' ~ strtotime('today')}) %}
<div class="stat-card stat-primary">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-content">
<h3 class="stat-number">{{ totalUsers | number }}</h3>
<p class="stat-label">{{ t('admin.total_users') }}</p>
<div class="stat-details">
<span class="text-success">
<i class="fas fa-check-circle"></i>
{{ activeUsers }} {{ t('admin.active') }}
</span>
</div>
</div>
</div>
<div class="stat-card stat-success">
<div class="stat-icon">
<i class="fas fa-circle"></i>
</div>
<div class="stat-content">
<h3 class="stat-number">{{ onlineUsers | number }}</h3>
<p class="stat-label">{{ t('admin.online_now') }}</p>
<div class="stat-details">
<small>{{ t('admin.last_5_minutes') }}</small>
</div>
</div>
</div>
<div class="stat-card stat-info">
<div class="stat-icon">
<i class="fas fa-user-plus"></i>
</div>
<div class="stat-content">
<h3 class="stat-number">{{ todayRegistrations | number }}</h3>
<p class="stat-label">{{ t('admin.today_registrations') }}</p>
{% set yesterdayRegs = db('1752665380840')->countItems('users', {
'created_at': '>' ~ strtotime('yesterday'),
'created_at': '<' ~ strtotime('today')
}) %}
<div class="stat-details">
{% if todayRegistrations > yesterdayRegs %}
<span class="text-success">
<i class="fas fa-arrow-up"></i>
{{ ((todayRegistrations - yesterdayRegs) / (yesterdayRegs | default(1)) * 100) | round }}%
</span>
{% else %}
<span class="text-danger">
<i class="fas fa-arrow-down"></i>
{{ ((yesterdayRegs - todayRegistrations) / (yesterdayRegs | default(1)) * 100) | round }}%
</span>
{% endif %}
</div>
</div>
</div>
{% set pendingComments = db('1752665380840')->countItems('comments', {'status': 'pending'}) %}
<div class="stat-card stat-warning">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-content">
<h3 class="stat-number">{{ pendingComments | number }}</h3>
<p class="stat-label">{{ t('admin.pending_comments') }}</p>
{% if pendingComments > 0 %}
<div class="stat-details">
<a href="{{ route('admin.comments', {'status': 'pending'}) }}"
class="btn btn-sm btn-warning">
{{ t('admin.review') }}
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="dashboard-grid">
<!-- Последние пользователи -->
<div class="dashboard-widget">
<div class="widget-header">
<h3>{{ t('admin.recent_users') }}</h3>
<a href="{{ route('admin.users') }}" class="btn btn-sm btn-outline-primary">
{{ t('admin.view_all') }}
</a>
</div>
<div class="widget-content">
{% set recentUsers = db('1752665380840')->getItems('users', {},
['id', 'login', 'fname', 'lname', 'email', 'avatar', 'created_at'],
'created_at', 'DESC', null, 5) %}
{% if recentUsers %}
<div class="user-list">
{% foreach recentUsers as user %}
<div class="user-item">
<div class="user-avatar">
<img src="{{ user.avatar ?? asset('images/default-avatar.png') }}"
alt="{{ user.login }}">
</div>
<div class="user-info">
<h5>{{ user.fname ~ ' ' ~ user.lname }}</h5>
<p>@{{ user.login }}</p>
<small>{{ user.created_at | date('d.m.Y H:i') }}</small>
</div>
<div class="user-actions">
<a href="{{ route('admin.user.view', {'id': user.id}) }}"
class="btn btn-sm btn-outline-info">
<i class="fas fa-eye"></i>
</a>
</div>
</div>
{% endforeach %}
</div>
{% else %}
<div class="empty-state">
<p>{{ t('admin.no_users') }}</p>
</div>
{% endif %}
</div>
</div>
<!-- Системная информация -->
<div class="dashboard-widget">
<div class="widget-header">
<h3>{{ t('admin.system_info') }}</h3>
</div>
<div class="widget-content">
<div class="system-stats">
<div class="system-item">
<span class="label">{{ t('system.php_version') }}:</span>
<span class="value">{{ phpversion() }}</span>
</div>
<div class="system-item">
<span class="label">{{ t('system.memory_usage') }}:</span>
<span class="value">{{ (memory_get_usage() / 1024 / 1024) | round(2) }} MB</span>
</div>
<div class="system-item">
<span class="label">{{ t('system.peak_memory') }}:</span>
<span class="value">{{ (memory_get_peak_usage() / 1024 / 1024) | round(2) }} MB</span>
</div>
<div class="system-item">
<span class="label">{{ t('system.execution_time') }}:</span>
<span class="value">{{ (microtime(true) - MNRFY_START_TIME) * 1000 | round(2) }} ms</span>
</div>
<div class="system-item">
<span class="label">{{ t('system.framework') }}:</span>
<span class="value">MNRFY {{ MNRFY_VERSION ?? '1.0' }}</span>
</div>
</div>
</div>
</div>
<!-- График активности -->
<div class="dashboard-widget dashboard-chart">
<div class="widget-header">
<h3>{{ t('admin.activity_chart') }}</h3>
<div class="chart-controls">
<select id="chartPeriod" onchange="updateChart(this.value)">
<option value="7">{{ t('admin.last_7_days') }}</option>
<option value="30">{{ t('admin.last_30_days') }}</option>
<option value="90">{{ t('admin.last_90_days') }}</option>
</select>
</div>
</div>
<div class="widget-content">
{% set activityData = db('1752665380840')->query('
SELECT
DATE(FROM_UNIXTIME(created_at)) as date,
COUNT(*) as registrations
FROM users
WHERE created_at >= ?
GROUP BY date
ORDER BY date DESC
LIMIT 7
', [strtotime('-7 days')]) %}
<canvas id="activityChart" width="400" height="200"></canvas>
{% set chartLabels = [] %}
{% set chartData = [] %}
{% foreach activityData | reverse as item %}
{% set chartLabels = chartLabels | push(item.date | date('d.m')) %}
{% set chartData = chartData | push(item.registrations) %}
{% endforeach %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('activityChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: {{ chartLabels | json }},
datasets: [{
label: '{{ t("admin.registrations") }}',
data: {{ chartData | json }},
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
});
</script>
</div>
</div>
</div>
</div>
{% endblock %}
{% block page_scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
function updateChart(days) {
// AJAX запрос для обновления данных графика
fetch('/admin/api/activity-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
},
body: JSON.stringify({days: days})
})
.then(response => response.json())
.then(data => {
// Обновление графика
console.log('Chart updated for', days, 'days');
});
}
</script>
{% endblock %}
Управление пользователями
admin-users.html
{% extends 'layouts/admin.html' %}
{% block title %}{{ t('admin.users') }} - {{ parent() }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active">{{ t('admin.users') }}</li>
{% endblock %}
{% block page_title %}
{{ t('admin.users') }}
<small class="text-muted">({{ users.total }} {{ t('admin.total') }})</small>
{% endblock %}
{% block page_actions %}
<div class="btn-group">
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="fas fa-plus"></i> {{ t('admin.add_user') }}
</button>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary dropdown-toggle"
data-bs-toggle="dropdown">
<i class="fas fa-filter"></i> {{ t('admin.filter') }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="?filter=all">{{ t('filter.all') }}</a></li>
<li><a class="dropdown-item" href="?filter=active">{{ t('status.active') }}</a></li>
<li><a class="dropdown-item" href="?filter=banned">{{ t('status.banned') }}</a></li>
<li><a class="dropdown-item" href="?filter=premium">{{ t('status.premium') }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="?filter=today">{{ t('filter.registered_today') }}</a></li>
</ul>
</div>
<button type="button" class="btn btn-outline-secondary" onclick="exportUsers()">
<i class="fas fa-download"></i> {{ t('admin.export') }}
</button>
</div>
{% endblock %}
{% block content %}
<!-- Быстрая статистика -->
<div class="quick-stats mb-4">
<div class="row">
<div class="col-md-3">
<div class="stat-box bg-primary">
<div class="stat-number">{{ db('1752665380840')->countItems('users') }}</div>
<div class="stat-label">{{ t('admin.total_users') }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-box bg-success">
<div class="stat-number">{{ db('1752665380840')->countItems('users', {'ban': 0}) }}</div>
<div class="stat-label">{{ t('admin.active_users') }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-box bg-warning">
<div class="stat-number">{{ db('1752665380840')->countItems('users', {'ban': 1}) }}</div>
<div class="stat-label">{{ t('admin.banned_users') }}</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-box bg-info">
<div class="stat-number">{{ db('1752665380840')->countItems('users', {'last_seen': '>' ~ (time() - 300)}) }}</div>
<div class="stat-label">{{ t('admin.online_now') }}</div>
</div>
</div>
</div>
</div>
<!-- Поиск и фильтры -->
<div class="filters-panel">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-4">
<label for="search" class="form-label">{{ t('admin.search') }}</label>
<div class="input-group">
<input type="text" id="search" name="search" class="form-control"
placeholder="{{ t('admin.search_users_placeholder') }}"
value="{{ request.input('search') }}">
<button class="btn btn-outline-secondary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-md-2">
<label for="role" class="form-label">{{ t('admin.role') }}</label>
<select id="role" name="role" class="form-select">
<option value="">{{ t('filter.all_roles') }}</option>
<option value="user" {{ request.input('role') == 'user' ? 'selected' : '' }}>{{ t('role.user') }}</option>
<option value="moderator" {{ request.input('role') == 'moderator' ? 'selected' : '' }}>{{ t('role.moderator') }}</option>
<option value="admin" {{ request.input('role') == 'admin' ? 'selected' : '' }}>{{ t('role.admin') }}</option>
</select>
</div>
<div class="col-md-2">
<label for="status" class="form-label">{{ t('admin.status') }}</label>
<select id="status" name="status" class="form-select">
<option value="">{{ t('filter.all_statuses') }}</option>
<option value="active" {{ request.input('status') == 'active' ? 'selected' : '' }}>{{ t('status.active') }}</option>
<option value="banned" {{ request.input('status') == 'banned' ? 'selected' : '' }}>{{ t('status.banned') }}</option>
</select>
</div>
<div class="col-md-2">
<label for="sort" class="form-label">{{ t('admin.sort_by') }}</label>
<select id="sort" name="sort" class="form-select">
<option value="created_at_desc" {{ request.input('sort') == 'created_at_desc' ? 'selected' : '' }}>{{ t('sort.newest') }}</option>
<option value="created_at_asc" {{ request.input('sort') == 'created_at_asc' ? 'selected' : '' }}>{{ t('sort.oldest') }}</option>
<option value="login_asc" {{ request.input('sort') == 'login_asc' ? 'selected' : '' }}>{{ t('sort.username_az') }}</option>
<option value="balance_desc" {{ request.input('sort') == 'balance_desc' ? 'selected' : '' }}>{{ t('sort.balance_high') }}</option>
<option value="last_seen_desc" {{ request.input('sort') == 'last_seen_desc' ? 'selected' : '' }}>{{ t('sort.last_active') }}</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-filter"></i> {{ t('button.apply') }}
</button>
</div>
</form>
</div>
<!-- Таблица пользователей -->
{% set page = request.input('page', 1) %}
{% set search = request.input('search') %}
{% set role = request.input('role') %}
{% set status = request.input('status') %}
{% set sort = request.input('sort', 'created_at_desc') %}
{% set whereConditions = {} %}
{% if role %}
{% set whereConditions = whereConditions | merge({'role': role}) %}
{% endif %}
{% if status == 'active' %}
{% set whereConditions = whereConditions | merge({'ban': 0}) %}
{% elseif status == 'banned' %}
{% set whereConditions = whereConditions | merge({'ban': 1}) %}
{% endif %}
{% set sortParts = sort | split('_') %}
{% set sortField = sortParts[0] ~ (sortParts[1] ? ('_' ~ sortParts[1]) : '') %}
{% set sortDirection = sortParts | last == 'desc' ? 'DESC' : 'ASC' %}
{% set users = db('1752665380840')->paginate(
'users', page, 25, whereConditions, sortField, sortDirection
) %}
<div class="users-table-container">
<div class="table-header">
<div class="bulk-actions" id="bulkActions" style="display: none;">
<div class="bulk-info">
<span id="selectedCount">0</span> {{ t('admin.users_selected') }}
</div>
<div class="bulk-buttons">
<button type="button" class="btn btn-sm btn-success" onclick="bulkAction('activate')">
<i class="fas fa-check"></i> {{ t('admin.activate') }}
</button>
<button type="button" class="btn btn-sm btn-warning" onclick="bulkAction('ban')">
<i class="fas fa-ban"></i> {{ t('admin.ban') }}
</button>
<button type="button" class="btn btn-sm btn-danger" onclick="bulkAction('delete')">
<i class="fas fa-trash"></i> {{ t('admin.delete') }}
</button>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover users-table">
<thead>
<tr>
<th width="40">
<input type="checkbox" class="form-check-input" id="selectAll"
onchange="toggleSelectAll(this)">
</th>
<th width="80">{{ t('admin.avatar') }}</th>
<th>{{ t('admin.user') }}</th>
<th>{{ t('admin.role') }}</th>
<th>{{ t('admin.balance') }}</th>
<th>{{ t('admin.status') }}</th>
<th>{{ t('admin.last_seen') }}</th>
<th>{{ t('admin.registered') }}</th>
<th width="150">{{ t('admin.actions') }}</th>
</tr>
</thead>
<tbody>
{% if users.data and users.data | length > 0 %}
{% foreach users.data as user %}
<tr class="user-row" data-user-id="{{ user.id }}">
<td>
<input type="checkbox" class="form-check-input user-checkbox"
value="{{ user.id }}" onchange="updateBulkActions()">
</td>
<td>
<img src="{{ user.avatar ?? asset('images/default-avatar.png') }}"
alt="{{ user.login }}" class="user-avatar-small">
</td>
<td>
<div class="user-info">
<div class="user-name">
<strong>{{ user.fname ~ ' ' ~ user.lname }}</strong>
{% if user.last_seen and (time() - user.last_seen) < 300 %}
<span class="online-indicator" title="{{ t('admin.online') }}"></span>
{% endif %}
</div>
<div class="user-login">@{{ user.login }}</div>
{% if user.email %}
<div class="user-email">{{ user.email }}</div>
{% endif %}
</div>
</td>
<td>
{% switch user.role %}
{% case 'admin' %}
<span class="badge bg-danger">
<i class="fas fa-crown"></i> {{ t('role.admin') }}
</span>
{% case 'moderator' %}
<span class="badge bg-warning">
<i class="fas fa-shield-alt"></i> {{ t('role.moderator') }}
</span>
{% default %}
<span class="badge bg-secondary">
<i class="fas fa-user"></i> {{ t('role.user') }}
</span>
{% endswitch %}
{% if user.is_premium %}
<span class="badge bg-info ms-1">
<i class="fas fa-star"></i> {{ t('status.premium') }}
</span>
{% endif %}
</td>
<td>
<span class="balance {{ user.balance > 0 ? 'text-success' : 'text-muted' }}">
{{ user.balance | money('₽') }}
</span>
</td>
<td>
{% if user.ban %}
<span class="badge bg-danger">
<i class="fas fa-ban"></i> {{ t('status.banned') }}
</span>
{% else %}
<span class="badge bg-success">
<i class="fas fa-check"></i> {{ t('status.active') }}
</span>
{% endif %}
{% if user.email_verified %}
<span class="badge bg-info ms-1" title="{{ t('status.email_verified') }}">
<i class="fas fa-check-circle"></i>
</span>
{% endif %}
</td>
<td>
{% if user.last_seen %}
{% set timeDiff = time() - user.last_seen %}
{% if timeDiff < 60 %}
<span class="text-success">{{ t('time.just_now') }}</span>
{% elseif timeDiff < 3600 %}
<span class="text-info">{{ (timeDiff / 60) | round }} {{ t('time.minutes_ago') }}</span>
{% elseif timeDiff < 86400 %}
<span class="text-warning">{{ (timeDiff / 3600) | round }} {{ t('time.hours_ago') }}</span>
{% else %}
<span class="text-muted">{{ user.last_seen | date('d.m.Y') }}</span>
{% endif %}
{% else %}
<span class="text-muted">{{ t('admin.never') }}</span>
{% endif %}
</td>
<td>
<span class="text-muted">{{ user.created_at | date('d.m.Y H:i') }}</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ route('admin.user.view', {'id': user.id}) }}"
class="btn btn-sm btn-outline-info"
title="{{ t('admin.view') }}">
<i class="fas fa-eye"></i>
</a>
<a href="{{ route('admin.user.edit', {'id': user.id}) }}"
class="btn btn-sm btn-outline-warning"
title="{{ t('admin.edit') }}">
<i class="fas fa-edit"></i>
</a>
{% if not user.ban %}
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="banUser({{ user.id }})"
title="{{ t('admin.ban') }}">
<i class="fas fa-ban"></i>
</button>
{% else %}
<button type="button" class="btn btn-sm btn-outline-success"
onclick="unbanUser({{ user.id }})"
title="{{ t('admin.unban') }}">
<i class="fas fa-check"></i>
</button>
{% endif %}
{% if user.role != 'admin' or auth().id == user.id %}
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="deleteUser({{ user.id }}, '{{ user.login }}')"
title="{{ t('admin.delete') }}">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endforeach %}
{% else %}
<tr>
<td colspan="9" class="text-center py-4">
<div class="empty-state">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h4>{{ t('admin.no_users_found') }}</h4>
<p class="text-muted">{{ t('admin.try_different_filters') }}</p>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if users.last_page > 1 %}
{% component 'pagination' with {
'data': users,
'url': request.path,
'preserve_query': true
} %}
{% endif %}
</div>
<!-- Модальное окно добавления пользователя -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ t('admin.add_user') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="addUserForm" method="POST" action="/" data-ajax="true">
{{ csrf() }}
<input type="hidden" name="__handler" value="admin-add-user">
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="login" class="form-label">{{ t('form.login') }} *</label>
<input type="text" id="login" name="login" class="form-control" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">{{ t('form.email') }} *</label>
<input type="email" id="email" name="email" class="form-control" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="fname" class="form-label">{{ t('form.first_name') }}</label>
<input type="text" id="fname" name="fname" class="form-control">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="lname" class="form-label">{{ t('form.last_name') }}</label>
<input type="text" id="lname" name="lname" class="form-control">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="password" class="form-label">{{ t('form.password') }} *</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="role" class="form-label">{{ t('form.role') }}</label>
<select id="role" name="role" class="form-select">
<option value="user">{{ t('role.user') }}</option>
<option value="moderator">{{ t('role.moderator') }}</option>
{% if auth().role == 'admin' %}
<option value="admin">{{ t('role.admin') }}</option>
{% endif %}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="balance" class="form-label">{{ t('form.balance') }}</label>
<input type="number" id="balance" name="balance" class="form-control"
value="0" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input type="checkbox" id="email_verified" name="email_verified"
class="form-check-input" value="1">
<label for="email_verified" class="form-check-label">
{{ t('form.email_verified') }}
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ t('button.cancel') }}
</button>
<button type="submit" class="btn btn-success">
<i class="fas fa-plus"></i> {{ t('admin.add_user') }}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block page_scripts %}
<script>
// Функции управления пользователями
function toggleSelectAll(checkbox) {
const userCheckboxes = document.querySelectorAll('.user-checkbox');
userCheckboxes.forEach(cb => cb.checked = checkbox.checked);
updateBulkActions();
}
function updateBulkActions() {
const selected = document.querySelectorAll('.user-checkbox:checked');
const bulkActions = document.getElementById('bulkActions');
const selectedCount = document.getElementById('selectedCount');
if (selected.length > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = selected.length;
} else {
bulkActions.style.display = 'none';
}
}
function bulkAction(action) {
const selected = Array.from(document.querySelectorAll('.user-checkbox:checked'))
.map(cb => cb.value);
if (selected.length === 0) {
alert('{{ t("admin.select_users_first") }}');
return;
}
let confirmMessage = '';
switch(action) {
case 'activate':
confirmMessage = '{{ t("admin.confirm_bulk_activate") }}';
break;
case 'ban':
confirmMessage = '{{ t("admin.confirm_bulk_ban") }}';
break;
case 'delete':
confirmMessage = '{{ t("admin.confirm_bulk_delete") }}';
break;
}
if (confirm(confirmMessage)) {
performBulkAction(action, selected);
}
}
function performBulkAction(action, userIds) {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
},
body: new URLSearchParams({
'__handler': 'admin-bulk-users',
'action': action,
'user_ids': JSON.stringify(userIds)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || '{{ t("admin.error_occurred") }}');
}
})
.catch(error => {
alert('{{ t("admin.network_error") }}');
});
}
function banUser(userId) {
if (confirm('{{ t("admin.confirm_ban_user") }}')) {
userAction(userId, 'ban');
}
}
function unbanUser(userId) {
if (confirm('{{ t("admin.confirm_unban_user") }}')) {
userAction(userId, 'unban');
}
}
function deleteUser(userId, username) {
if (confirm('{{ t("admin.confirm_delete_user") }}'.replace(':username', username))) {
userAction(userId, 'delete');
}
}
function userAction(userId, action) {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
},
body: new URLSearchParams({
'__handler': 'admin-user-action',
'user_id': userId,
'action': action
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || '{{ t("admin.error_occurred") }}');
}
});
}
function exportUsers() {
const params = new URLSearchParams(window.location.search);
params.set('export', '1');
window.open('?' + params.toString());
}
// Обработка AJAX формы добавления пользователя
document.getElementById('addUserForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('[type="submit"]');
const originalText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> {{ t("form.saving") }}';
fetch('/', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('addUserModal'));
modal.hide();
// Показываем уведомление об успехе
showNotification(data.message || '{{ t("admin.user_created_successfully") }}', 'success');
// Перезагружаем страницу через секунду
setTimeout(() => location.reload(), 1000);
} else {
if (data.errors) {
// Показываем ошибки валидации
Object.keys(data.errors).forEach(field => {
const input = document.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
let errorDiv = input.parentNode.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentNode.appendChild(errorDiv);
}
errorDiv.textContent = data.errors[field];
}
});
} else {
showNotification(data.error || '{{ t("admin.error_occurred") }}', 'error');
}
}
})
.catch(error => {
showNotification('{{ t("admin.network_error") }}', 'error');
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalText;
});
});
// Очистка ошибок при изменении полей
document.querySelectorAll('#addUserForm input, #addUserForm select').forEach(input => {
input.addEventListener('input', function() {
this.classList.remove('is-invalid');
const errorDiv = this.parentNode.querySelector('.invalid-feedback');
if (errorDiv) {
errorDiv.remove();
}
});
});
// Функция показа уведомлений
function showNotification(message, type) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
const alert = document.createElement('div');
alert.className = `alert ${alertClass} alert-dismissible position-fixed`;
alert.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
alert.innerHTML = `
<i class="fas ${icon}"></i> ${message}
<button type="button" class="btn-close" onclick="this.parentElement.remove()"></button>
`;
document.body.appendChild(alert);
// Автоматическое скрытие через 5 секунд
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 5000);
}
</script>
{% endblock %}
Готово! Теперь у вас есть полнофункциональная админ-панель. Изучите также пользовательские обработчики для создания соответствующих PHP обработчиков форм.