add superadmin dashboard. subscriptions

This commit is contained in:
root
2026-01-13 20:03:16 +03:00
parent 3c24c250e5
commit edb4df7e37
36 changed files with 3329 additions and 100 deletions
+116
View File
@@ -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 %}
+91
View File
@@ -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 %}
+95
View File
@@ -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="Неограниченные проекты&#10;Приоритетная поддержка&#10;Экспорт в 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 %}
+96
View File
@@ -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="Неограниченные проекты&#10;Приоритетная поддержка&#10;Экспорт в 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 %}
+77
View File
@@ -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 %}
+179
View File
@@ -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 %}
+47
View File
@@ -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 %}