add superadmin dashboard. subscriptions
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Дашборд</h1>
|
||||
<div class="user-menu">
|
||||
<span>Добро пожаловать, {{ session_data.name }}</span>
|
||||
{{ get_avatar(null, 40, '') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for alert in get_alerts() %}
|
||||
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Всего пользователей</h3>
|
||||
<div class="value">{{ stats.total_users|number_format(0, '', ' ') }}</div>
|
||||
<div class="icon">👥</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Всего организаций</h3>
|
||||
<div class="value">{{ stats.total_orgs|number_format(0, '', ' ') }}</div>
|
||||
<div class="icon">🏢</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Зарегистрировано сегодня</h3>
|
||||
<div class="value">{{ stats.active_today|number_format(0, '', ' ') }}</div>
|
||||
<div class="icon">📅</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Всего тарифов</h3>
|
||||
<div class="value">{{ stats.total_plans|number_format(0, '', ' ') }}</div>
|
||||
<div class="icon">📋</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Последние организации</h2>
|
||||
<a href="{{ base_url('/superadmin/organizations') }}" class="btn btn-primary btn-sm">Все организации</a>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
{% if recentOrgs is empty %}
|
||||
<p style="color: #7f8c8d; text-align: center; padding: 20px;">Организаций пока нет</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Тип</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for org in recentOrgs %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ base_url('/superadmin/organizations/view/' ~ org.id) }}" style="color: #3498db; text-decoration: none;">
|
||||
{{ org.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if org.type == 'business' %}
|
||||
<span class="badge badge-info">Бизнес</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">Личное</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ org.created_at|date('d.m.Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Последние пользователи</h2>
|
||||
<a href="{{ base_url('/superadmin/users') }}" class="btn btn-primary btn-sm">Все пользователи</a>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
{% if recentUsers is empty %}
|
||||
<p style="color: #7f8c8d; text-align: center; padding: 20px;">Пользователей пока нет</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Роль</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in recentUsers %}
|
||||
<tr>
|
||||
<td>{{ user.name|default('—') }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<span class="badge {{ user.system_role == 'superadmin' ? 'badge-danger' : (user.system_role == 'admin' ? 'badge-warning' : 'badge-success') }}">
|
||||
{{ user.system_role|default('user') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Панель суперадмина{% endblock %} — Бизнес.Точка</title>
|
||||
<link href="{{ base_url('assets/css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ base_url('assets/css/all.min.css') }}" rel="stylesheet">
|
||||
<style>
|
||||
.sa-layout { display: flex; min-height: 100vh; }
|
||||
.sa-sidebar { width: 260px; background: #1a1a2e; color: #fff; padding: 20px 0; flex-shrink: 0; }
|
||||
.sa-sidebar .logo { padding: 0 20px 30px; font-size: 24px; font-weight: bold; border-bottom: 1px solid #333; margin-bottom: 20px; }
|
||||
.sa-sidebar .logo a { color: #fff; text-decoration: none; }
|
||||
.sa-sidebar nav ul { list-style: none; padding: 0; margin: 0; }
|
||||
.sa-sidebar nav ul li { border-bottom: 1px solid #252540; }
|
||||
.sa-sidebar nav ul li a { display: block; padding: 15px 20px; color: #aaa; text-decoration: none; transition: 0.3s; }
|
||||
.sa-sidebar nav ul li a:hover, .sa-sidebar nav ul li a.active { background: #252540; color: #fff; padding-left: 25px; }
|
||||
.sa-sidebar nav ul li a i { margin-right: 10px; }
|
||||
.sa-content { flex: 1; padding: 30px; background: #f5f6fa; overflow-y: auto; }
|
||||
.sa-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
|
||||
.sa-header h1 { margin: 0; font-size: 28px; color: #2c3e50; }
|
||||
.sa-header .user-menu { display: flex; align-items: center; gap: 15px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.stat-card { background: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
|
||||
.stat-card h3 { margin: 0 0 10px; font-size: 14px; color: #7f8c8d; text-transform: uppercase; }
|
||||
.stat-card .value { font-size: 36px; font-weight: bold; color: #2c3e50; }
|
||||
.stat-card .icon { float: right; font-size: 40px; opacity: 0.1; }
|
||||
.sa-card { background: #fff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 20px; }
|
||||
.sa-card-header { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
||||
.sa-card-header h2 { margin: 0; font-size: 18px; color: #2c3e50; }
|
||||
.sa-card-body { padding: 20px; }
|
||||
.btn { display: inline-block; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; text-decoration: none; font-size: 14px; transition: 0.3s; }
|
||||
.btn-primary { background: #3498db; color: #fff; }
|
||||
.btn-primary:hover { background: #2980b9; }
|
||||
.btn-success { background: #27ae60; color: #fff; }
|
||||
.btn-success:hover { background: #219a52; }
|
||||
.btn-danger { background: #e74c3c; color: #fff; }
|
||||
.btn-danger:hover { background: #c0392b; }
|
||||
.btn-warning { background: #f39c12; color: #fff; }
|
||||
.btn-warning:hover { background: #d68910; }
|
||||
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
||||
.btn-block { display: block; width: 100%; text-align: center; }
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
|
||||
.table th { background: #f8f9fa; font-weight: 600; color: #2c3e50; }
|
||||
.table tr:hover { background: #f8f9fa; }
|
||||
.badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; }
|
||||
.badge-success { background: #d4edda; color: #155724; }
|
||||
.badge-warning { background: #fff3cd; color: #856404; }
|
||||
.badge-danger { background: #f8d7da; color: #721c24; }
|
||||
.badge-info { background: #d1ecf1; color: #0c5460; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #2c3e50; }
|
||||
.form-control { width: 100%; padding: 12px 15px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; }
|
||||
.form-control:focus { outline: none; border-color: #3498db; }
|
||||
.alert { padding: 15px 20px; border-radius: 5px; margin-bottom: 20px; }
|
||||
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.alert-danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.alert-warning { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
@media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="sa-layout">
|
||||
<aside class="sa-sidebar">
|
||||
<div class="logo">
|
||||
<a href="{{ base_url('/superadmin') }}">⚡ Business.Point</a>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ base_url('/superadmin') }}" class="{{ is_active_route('superadmin') and not is_active_route('superadmin/') ? 'active' : '' }}">📊 Дашборд</a></li>
|
||||
<li><a href="{{ base_url('/superadmin/plans') }}" class="{{ is_active_route('superadmin/plans') ? 'active' : '' }}">📋 Тарифы</a></li>
|
||||
<li><a href="{{ base_url('/superadmin/organizations') }}" class="{{ is_active_route('superadmin/organizations') ? 'active' : '' }}">🏢 Организации</a></li>
|
||||
<li><a href="{{ base_url('/superadmin/users') }}" class="{{ is_active_route('superadmin/users') ? 'active' : '' }}">👥 Пользователи</a></li>
|
||||
<li><a href="{{ base_url('/superadmin/statistics') }}" class="{{ is_active_route('superadmin/statistics') ? 'active' : '' }}">📈 Статистика</a></li>
|
||||
<li><hr style="border-color: #333; margin: 15px 20px;"></li>
|
||||
<li><a href="{{ base_url('/') }}" target="_blank">🔗 Вернуться на сайт</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="sa-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<script src="{{ base_url('assets/js/bootstrap.bundle.min.js') }}"></script>
|
||||
<script src="{{ base_url('assets/js/base.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Организации</h1>
|
||||
</div>
|
||||
|
||||
{% for alert in get_alerts() %}
|
||||
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-body">
|
||||
<div id="organizations-table">
|
||||
{# Динамическая таблица #}
|
||||
{{ tableHtml|raw }}
|
||||
{# CSRF токен для AJAX запросов #}
|
||||
{{ csrf_field()|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
|
||||
<script src="/assets/js/modules/DataTable.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация DataTable
|
||||
document.querySelectorAll('.data-table').forEach(function(container) {
|
||||
const id = container.id;
|
||||
const url = container.dataset.url;
|
||||
const perPage = parseInt(container.dataset.perPage) || 10;
|
||||
|
||||
if (window.dataTables && window.dataTables[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new DataTable(id, {
|
||||
url: url,
|
||||
perPage: perPage
|
||||
});
|
||||
|
||||
window.dataTables = window.dataTables || {};
|
||||
window.dataTables[id] = table;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,220 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Организация: {{ organization.name }}</h1>
|
||||
<a href="{{ base_url('/superadmin/organizations') }}" class="btn btn-primary">← Назад</a>
|
||||
</div>
|
||||
|
||||
{% for alert in get_alerts() %}
|
||||
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Информация об организации</h2>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th style="width: 150px;">ID</th>
|
||||
<td>{{ organization.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<td>{{ organization.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Тип</th>
|
||||
<td>
|
||||
{% if organization.type == 'business' %}
|
||||
<span class="badge badge-info">Бизнес</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">Личное пространство</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Тариф</th>
|
||||
<td>
|
||||
{% if currentSubscription %}
|
||||
{% set plan = plans|filter(p => p.id == currentSubscription.plan_id)|first %}
|
||||
{% if plan %}
|
||||
<span class="badge badge-primary">{{ plan.name }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">ID: {{ currentSubscription.plan_id }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Не назначен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Статус подписки</th>
|
||||
<td>
|
||||
{% if currentSubscription %}
|
||||
{% if currentSubscription.status == 'active' %}
|
||||
<span class="badge badge-success">Активна</span>
|
||||
{% elseif currentSubscription.status == 'expired' %}
|
||||
<span class="badge badge-danger">Истёкшая</span>
|
||||
{% elseif currentSubscription.status == 'trial' %}
|
||||
<span class="badge badge-info">Пробный период</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">{{ currentSubscription.status }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge badge-secondary">Нет подписки</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Срок действия</th>
|
||||
<td>
|
||||
{% if currentSubscription and currentSubscription.expires_at %}
|
||||
до {{ currentSubscription.expires_at|date('d.m.Y H:i') }}
|
||||
{% else %}
|
||||
Не ограничен
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Статус организации</th>
|
||||
<td>
|
||||
{% if organization.status == 'active' %}
|
||||
<span class="badge badge-success">Активна</span>
|
||||
{% elseif organization.status == 'blocked' %}
|
||||
<span class="badge badge-danger">Заблокирована</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">{{ organization.status|default('Не определён') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Создана</th>
|
||||
<td>{{ organization.created_at|date('d.m.Y H:i') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
{% if organization.status == 'active' %}
|
||||
<a href="{{ base_url('/superadmin/organizations/block/' ~ organization.id) }}" class="btn btn-warning" onclick="return confirm('Заблокировать организацию?')">🚫 Заблокировать</a>
|
||||
{% else %}
|
||||
<a href="{{ base_url('/superadmin/organizations/unblock/' ~ organization.id) }}" class="btn btn-success" onclick="return confirm('Разблокировать организацию?')">✅ Разблокировать</a>
|
||||
{% endif %}
|
||||
<a href="{{ base_url('/superadmin/organizations/delete/' ~ organization.id) }}" class="btn btn-danger" onclick="return confirm('Удалить организацию? Это действие нельзя отменить!')">🗑️ Удалить</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Управление тарифом</h2>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
<form action="{{ base_url('/superadmin/organizations/set-plan/' ~ organization.id) }}" method="post">
|
||||
{{ csrf_field()|raw }}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="plan_id">Выберите тариф</label>
|
||||
<select name="plan_id" id="plan_id" class="form-control" required>
|
||||
<option value="">-- Выберите тариф --</option>
|
||||
{% for plan in plans %}
|
||||
<option value="{{ plan.id }}" {{ currentSubscription and currentSubscription.plan_id == plan.id ? 'selected' : '' }}>
|
||||
{{ plan.name }} - {{ plan.price }} {{ plan.currency }}/{{ plan.billing_period }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="duration_days">Срок действия (дней)</label>
|
||||
<input type="number" name="duration_days" id="duration_days" class="form-control"
|
||||
value="30" min="0" max="365000" placeholder="30">
|
||||
<small class="text-muted">Оставьте пустым или 0 для неограниченного срока</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Назначить тариф</button>
|
||||
</form>
|
||||
|
||||
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
||||
|
||||
<h4>Статистика</h4>
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<div style="font-size: 48px; font-weight: bold; color: #3498db;">{{ users|length }}</div>
|
||||
<div style="color: #7f8c8d;">Участников</div>
|
||||
</div>
|
||||
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
||||
<div style="display: flex; justify-content: space-around; text-align: center;">
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: bold; color: #27ae60;">
|
||||
{% set owner_count = users|filter(u => u.role == 'owner')|length %}
|
||||
{{ owner_count }}
|
||||
</div>
|
||||
<div style="color: #7f8c8d; font-size: 12px;">Владельцы</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: bold; color: #f39c12;">
|
||||
{% set admin_count = users|filter(u => u.role == 'admin')|length %}
|
||||
{{ admin_count }}
|
||||
</div>
|
||||
<div style="color: #7f8c8d; font-size: 12px;">Админы</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 24px; font-weight: bold; color: #9b59b6;">
|
||||
{% set manager_count = users|filter(u => u.role == 'manager')|length %}
|
||||
{{ manager_count }}
|
||||
</div>
|
||||
<div style="color: #7f8c8d; font-size: 12px;">Менеджеры</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-card" style="margin-top: 20px;">
|
||||
<div class="sa-card-header">
|
||||
<h2>Участники организации</h2>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
{% if users is empty %}
|
||||
<p style="color: #7f8c8d; text-align: center; padding: 40px;">Участников пока нет</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Пользователь</th>
|
||||
<th>Email</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата добавления</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.name|default('—') }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<span class="badge {{ user.role == 'owner' ? 'badge-danger' : (user.role == 'admin' ? 'badge-warning' : 'badge-info') }}">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.status == 'active' %}
|
||||
<span class="badge badge-success">Активен</span>
|
||||
{% elseif user.status == 'blocked' %}
|
||||
<span class="badge badge-danger">Заблокирован</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">{{ user.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.created_at|date('d.m.Y') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Создание тарифа</h1>
|
||||
<a href="{{ base_url('/superadmin/plans') }}" class="btn btn-primary">← Назад</a>
|
||||
</div>
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-body">
|
||||
<form action="{{ base_url('/superadmin/plans/store') }}" method="post">
|
||||
{{ csrf_field()|raw }}
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="name">Название тарифа *</label>
|
||||
<input type="text" name="name" id="name" class="form-control" value="{{ old('name') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="price">Цена *</label>
|
||||
<input type="number" name="price" id="price" class="form-control" value="{{ old('price', 0) }}" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание</label>
|
||||
<textarea name="description" id="description" class="form-control" rows="3">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select name="currency" id="currency" class="form-control">
|
||||
<option value="RUB" {{ old('currency', 'RUB') == 'RUB' ? 'selected' : '' }}>Рубль (RUB)</option>
|
||||
<option value="USD" {{ old('currency') == 'USD' ? 'selected' : '' }}>Доллар (USD)</option>
|
||||
<option value="EUR" {{ old('currency') == 'EUR' ? 'selected' : '' }}>Евро (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="billing_period">Период оплаты</label>
|
||||
<select name="billing_period" id="billing_period" class="form-control">
|
||||
<option value="monthly" {{ old('billing_period', 'monthly') == 'monthly' ? 'selected' : '' }}>Ежемесячно</option>
|
||||
<option value="yearly" {{ old('billing_period') == 'yearly' ? 'selected' : '' }}>Ежегодно</option>
|
||||
<option value="quarterly" {{ old('billing_period') == 'quarterly' ? 'selected' : '' }}>Ежеквартально</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="max_users">Максимум пользователей *</label>
|
||||
<input type="number" name="max_users" id="max_users" class="form-control" value="{{ old('max_users', 5) }}" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_clients">Максимум клиентов *</label>
|
||||
<input type="number" name="max_clients" id="max_clients" class="form-control" value="{{ old('max_clients', 100) }}" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_storage">Максимум хранилища (ГБ) *</label>
|
||||
<input type="number" name="max_storage" id="max_storage" class="form-control" value="{{ old('max_storage', 10) }}" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="features">Возможности (каждая с новой строки)</label>
|
||||
<textarea name="features_list" id="features" class="form-control" rows="5" placeholder="Неограниченные проекты Приоритетная поддержка Экспорт в PDF">{{ old('features_list') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1" {{ old('is_active', 1) ? 'checked' : '' }}>
|
||||
Активен (доступен для выбора)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_default" value="1" {{ old('is_default') ? 'checked' : '' }}>
|
||||
Тариф по умолчанию
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-success btn-block">Создать тариф</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Редактирование тарифа: {{ plan.name }}</h1>
|
||||
<a href="{{ base_url('/superadmin/plans') }}" class="btn btn-primary">← Назад</a>
|
||||
</div>
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-body">
|
||||
<form action="{{ base_url('/superadmin/plans/update/' ~ plan.id) }}" method="post">
|
||||
{{ csrf_field()|raw }}
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="name">Название тарифа *</label>
|
||||
<input type="text" name="name" id="name" class="form-control" value="{{ plan.name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="price">Цена *</label>
|
||||
<input type="number" name="price" id="price" class="form-control" value="{{ plan.price }}" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Описание</label>
|
||||
<textarea name="description" id="description" class="form-control" rows="3">{{ plan.description|default('') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select name="currency" id="currency" class="form-control">
|
||||
<option value="RUB" {{ plan.currency == 'RUB' ? 'selected' : '' }}>Рубль (RUB)</option>
|
||||
<option value="USD" {{ plan.currency == 'USD' ? 'selected' : '' }}>Доллар (USD)</option>
|
||||
<option value="EUR" {{ plan.currency == 'EUR' ? 'selected' : '' }}>Евро (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="billing_period">Период оплаты</label>
|
||||
<select name="billing_period" id="billing_period" class="form-control">
|
||||
<option value="monthly" {{ plan.billing_period == 'monthly' ? 'selected' : '' }}>Ежемесячно</option>
|
||||
<option value="yearly" {{ plan.billing_period == 'yearly' ? 'selected' : '' }}>Ежегодно</option>
|
||||
<option value="quarterly" {{ plan.billing_period == 'quarterly' ? 'selected' : '' }}>Ежеквартально</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label for="max_users">Максимум пользователей *</label>
|
||||
<input type="number" name="max_users" id="max_users" class="form-control" value="{{ plan.max_users }}" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_clients">Максимум клиентов *</label>
|
||||
<input type="number" name="max_clients" id="max_clients" class="form-control" value="{{ plan.max_clients }}" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_storage">Максимум хранилища (ГБ) *</label>
|
||||
<input type="number" name="max_storage" id="max_storage" class="form-control" value="{{ plan.max_storage }}" min="1" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="features">Возможности (каждая с новой строки)</label>
|
||||
<textarea name="features_list" id="features" class="form-control" rows="5" placeholder="Неограниченные проекты Приоритетная поддержка Экспорт в PDF">{% if plan.features is iterable %}{% for feature in plan.features %}
|
||||
{{ feature }}{% endfor %}{% endif %}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1" {{ plan.is_active ? 'checked' : '' }}>
|
||||
Активен (доступен для выбора)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_default" value="1" {{ plan.is_default ? 'checked' : '' }}>
|
||||
Тариф по умолчанию
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-success btn-block">Сохранить изменения</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Тарифы</h1>
|
||||
<a href="{{ base_url('/superadmin/plans/create') }}" class="btn btn-success">+ Добавить тариф</a>
|
||||
</div>
|
||||
|
||||
{% for alert in get_alerts() %}
|
||||
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-body">
|
||||
{% if plans is empty %}
|
||||
<p style="color: #7f8c8d; text-align: center; padding: 40px;">Тарифов пока нет. Создайте первый тариф.</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Цена</th>
|
||||
<th>Период</th>
|
||||
<th>Лимиты</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plan in plans %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ plan.name }}</strong>
|
||||
{% if plan.is_default %}
|
||||
<span class="badge badge-success">По умолчанию</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ plan.description|default('—') }}</td>
|
||||
<td>
|
||||
<strong>{{ plan.price|number_format(0, '', ' ') }}</strong> {{ plan.currency }}
|
||||
</td>
|
||||
<td>
|
||||
{% if plan.billing_period == 'monthly' %}
|
||||
Месяц
|
||||
{% elseif plan.billing_period == 'yearly' %}
|
||||
Год
|
||||
{% else %}
|
||||
{{ plan.billing_period }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small style="color: #7f8c8d;">
|
||||
👥 до {{ plan.max_users }} чел.<br>
|
||||
👤 до {{ plan.max_clients }} клиентов<br>
|
||||
💾 до {{ plan.max_storage }} ГБ
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if plan.is_active %}
|
||||
<span class="badge badge-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ base_url('/superadmin/plans/edit/' ~ plan.id) }}" class="btn btn-primary btn-sm">✏️</a>
|
||||
<a href="{{ base_url('/superadmin/plans/delete/' ~ plan.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Вы уверены?')">🗑️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,179 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Статистика</h1>
|
||||
</div>
|
||||
<div class="grid-2" style="margin-top: 20px;">
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Распределение по тарифам</h2>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
{% if planStats is empty %}
|
||||
<p style="color: #7f8c8d; text-align: center; padding: 40px;">Нет данных о тарифах</p>
|
||||
{% else %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тариф</th>
|
||||
<th>Организаций</th>
|
||||
<th>Доля</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set totalOrgs = 0 %}
|
||||
{% for plan in planStats %}
|
||||
{% set totalOrgs = totalOrgs + plan.orgs_count %}
|
||||
{% endfor %}
|
||||
{% for plan in planStats %}
|
||||
{% set percent = totalOrgs > 0 ? (plan.orgs_count / totalOrgs * 100)|round(1) : 0 %}
|
||||
<tr>
|
||||
<td>{{ plan.name }}</td>
|
||||
<td>{{ plan.orgs_count }}</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div style="flex: 1; background: #eee; height: 20px; border-radius: 10px; overflow: hidden;">
|
||||
<div style="width: {{ percent }}%; background: #3498db; height: 100%;"></div>
|
||||
</div>
|
||||
<span style="min-width: 40px;">{{ percent }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Сводка</h2>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
<div style="padding: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; padding: 15px 0; border-bottom: 1px solid #eee;">
|
||||
<span>Всего пользователей (30 дней)</span>
|
||||
<strong>
|
||||
{% set totalUsers = 0 %}
|
||||
{% for stat in dailyStats %}
|
||||
{% set totalUsers = totalUsers + stat.users %}
|
||||
{% endfor %}
|
||||
{{ totalUsers|number_format(0, '', ' ') }}
|
||||
</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; padding: 15px 0; border-bottom: 1px solid #eee;">
|
||||
<span>Всего организаций (30 дней)</span>
|
||||
<strong>
|
||||
{% set totalOrgs = 0 %}
|
||||
{% for stat in dailyStats %}
|
||||
{% set totalOrgs = totalOrgs + stat.orgs %}
|
||||
{% endfor %}
|
||||
{{ totalOrgs|number_format(0, '', ' ') }}
|
||||
</strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; padding: 15px 0; border-bottom: 1px solid #eee;">
|
||||
<span>Среднее организаций на пользователя</span>
|
||||
<strong>
|
||||
{{ totalUsers > 0 ? (totalOrgs / totalUsers)|round(2) : 0 }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-header">
|
||||
<h2>Регистрации по дням (последние 30 дней)</h2>
|
||||
</div>
|
||||
<div style="margin-bottom: 30px;">
|
||||
<canvas id="statsChart" width="800" height="300"></canvas>
|
||||
</div>
|
||||
<div class="sa-card-body">
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Дата</th>
|
||||
<th>Новые пользователи</th>
|
||||
<th>Новые организации</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in dailyStats %}
|
||||
<tr>
|
||||
<td>{{ stat.date|date('d.m.Y') }}</td>
|
||||
<td>{{ stat.users }}</td>
|
||||
<td>{{ stat.orgs }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('statsChart').getContext('2d');
|
||||
|
||||
const labels = [
|
||||
{% for stat in dailyStats %}
|
||||
'{{ stat.date|date('d.m') }}'{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
const usersData = [
|
||||
{% for stat in dailyStats %}
|
||||
{{ stat.users }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
const orgsData = [
|
||||
{% for stat in dailyStats %}
|
||||
{{ stat.orgs }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Пользователи',
|
||||
data: usersData,
|
||||
borderColor: '#3498db',
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Организации',
|
||||
data: orgsData,
|
||||
borderColor: '#27ae60',
|
||||
backgroundColor: 'rgba(39, 174, 96, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends 'superadmin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sa-header">
|
||||
<h1>Пользователи</h1>
|
||||
</div>
|
||||
|
||||
{% for alert in get_alerts() %}
|
||||
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="sa-card">
|
||||
<div class="sa-card-body">
|
||||
<div id="users-table">
|
||||
{# Динамическая таблица #}
|
||||
{{ tableHtml|raw }}
|
||||
{# CSRF токен для AJAX запросов #}
|
||||
{{ csrf_field()|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
|
||||
<script src="/assets/js/modules/DataTable.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация DataTable
|
||||
document.querySelectorAll('.data-table').forEach(function(container) {
|
||||
const id = container.id;
|
||||
const url = container.dataset.url;
|
||||
const perPage = parseInt(container.dataset.perPage) || 10;
|
||||
|
||||
if (window.dataTables && window.dataTables[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new DataTable(id, {
|
||||
url: url,
|
||||
perPage: perPage
|
||||
});
|
||||
|
||||
window.dataTables = window.dataTables || {};
|
||||
window.dataTables[id] = table;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user