Many fixes

This commit is contained in:
root
2026-01-16 21:58:12 +03:00
parent b810a17649
commit 77f76c8c28
45 changed files with 3392 additions and 1243 deletions
+80 -1
View File
@@ -101,7 +101,86 @@
</div>
</div>
{# Легенда #}
{% block stylesheets %}
<style>
.calendar {
width: 100%;
}
.calendar .calendar-header {
display: grid !important;
grid-template-columns: repeat(7, 1fr) !important;
border-bottom: 1px solid #e5e7eb;
}
.calendar .calendar-header-cell {
padding: 0.75rem 0.5rem;
font-weight: 500;
color: #6b7280;
font-size: 0.875rem;
text-align: center;
}
.calendar .calendar-grid {
display: grid !important;
grid-template-columns: repeat(7, 1fr) !important;
}
.calendar .calendar-cell {
min-height: 100px;
padding: 0.5rem;
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
display: block !important;
}
.calendar .calendar-cell:nth-child(7n) {
border-right: none;
}
.calendar .calendar-cell.bg-light {
background-color: #f9fafb;
}
.calendar .calendar-cell-today {
background-color: #eff6ff;
}
.calendar .calendar-day-number {
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.calendar .calendar-events {
display: flex !important;
flex-direction: column;
gap: 0.25rem;
}
.calendar .calendar-event {
display: block !important;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
background-color: #f3f4f6;
border-left: 3px solid #6b7280;
border-radius: 0.25rem;
text-decoration: none;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar .calendar-event:hover {
background-color: #e5e7eb;
}
.calendar .calendar-events-more {
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
}
</style>
{% endblock %}
{% if showLegend|default(true) and (legend is defined or events is defined) %}
<div class="card shadow-sm mt-4">
<div class="card-body">
+42 -2
View File
@@ -131,20 +131,42 @@ function handleDrop(e) {
if (itemId && newColumnId) {
if (moveUrl) {
// AJAX перемещение
console.log('Moving deal:', itemId, 'to stage:', newColumnId);
// Находим перетаскиваемую карточку
const draggedCard = document.querySelector(`.kanban-card[data-item-id="${itemId}"]`);
const sourceColumn = draggedCard ? draggedCard.closest('.kanban-cards-container') : null;
// AJAX перемещение - base.js автоматически добавит CSRF заголовок
fetch(moveUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'item_id=' + itemId + '&column_id=' + newColumnId
body: 'deal_id=' + itemId + '&stage_id=' + newColumnId
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (onMove) {
window[onMove](itemId, newColumnId, data);
} else if (draggedCard && sourceColumn) {
// Перемещаем карточку в новую колонку
const targetContainer = this;
targetContainer.appendChild(draggedCard);
// Обновляем счётчики колонок
updateColumnCounters(sourceColumn);
updateColumnCounters(targetContainer);
// Анимация успешного перемещения
draggedCard.style.transition = 'all 0.2s ease';
draggedCard.style.transform = 'scale(1.02)';
setTimeout(() => {
draggedCard.style.transform = '';
}, 200);
} else {
location.reload();
}
@@ -162,4 +184,22 @@ function handleDrop(e) {
}
}
}
/**
* Обновление счётчиков колонки (количество карточек и сумма)
*/
function updateColumnCounters(container) {
const columnId = container.dataset.columnId;
const cards = container.querySelectorAll('.kanban-card');
const count = cards.length;
// Находим badge в заголовке колонки
const column = container.closest('.kanban-column');
if (column) {
const badge = column.querySelector('.badge');
if (badge) {
badge.textContent = count;
}
}
}
</script>
+14 -3
View File
@@ -16,10 +16,21 @@
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100 text-center p-3">
<div class="card-body">
<i class="fa-solid fa-users fa-3x text-primary mb-3"></i>
<h5 class="card-title">CRM</h5>
<i class="fa-solid fa-building fa-3x text-primary mb-3"></i>
<h5 class="card-title">Клиенты</h5>
<p class="card-text text-muted">Управление клиентами</p>
<a href="#" class="btn btn-outline-primary btn-sm">Скоро</a>
<a href="/clients" class="btn btn-outline-primary btn-sm">Открыть</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100 text-center p-3">
<div class="card-body">
<i class="fa-solid fa-chart-line fa-3x text-primary mb-3"></i>
<h5 class="card-title">CRM</h5>
<p class="card-text">Управление сделками</p>
<a href="/crm" class="btn btn-outline-primary btn-sm">Открыть</a>
</div>
</div>
</div>
+47 -8
View File
@@ -37,23 +37,62 @@
<a href="{{ base_url('/clients') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route('clients') ? 'active' : '' }}">
<i class="fa-solid fa-users me-2"></i> Клиенты
<i class="fa-solid fa-building text-primary me-2"></i> Клиенты
</a>
{# Модули #}
<!-- Будущие модули -->
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled">
<i class="fa-solid fa-chart-line me-2"></i> CRM
{# CRM модуль #}
{% if is_module_available('crm') %}
<a href="{{ base_url('/crm') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route('crm') ? 'active' : '' }}">
<i class="fa-solid fa-chart-line text-success me-2"></i> CRM
</a>
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled">
{% else %}
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled" title="Модуль CRM не активен">
<i class="fa-solid fa-lock text-muted me-2"></i> CRM
</a>
{% endif %}
{# Booking модуль #}
{% if is_module_available('booking') %}
<a href="{{ base_url('/booking') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route('booking') ? 'active' : '' }}">
<i class="fa-solid fa-calendar me-2"></i> Booking
</a>
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled">
<i class="fa-solid fa-file-contract me-2"></i> Proof
{% else %}
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled" title="Модуль Бронирования не активен">
<i class="fa-solid fa-lock text-muted me-2"></i> Booking
</a>
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled">
{% endif %}
{# Tasks модуль #}
{% if is_module_available('tasks') %}
<a href="{{ base_url('/tasks') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route('tasks') ? 'active' : '' }}">
<i class="fa-solid fa-check-square me-2"></i> Tasks
</a>
{% else %}
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled" title="Модуль Задачи не активен">
<i class="fa-solid fa-lock text-muted me-2"></i> Tasks
</a>
{% endif %}
{# Proof модуль #}
{% if is_module_available('proof') %}
<a href="{{ base_url('/proof') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route('proof') ? 'active' : '' }}">
<i class="fa-solid fa-file-contract me-2"></i> Proof
</a>
{% else %}
<a href="#" class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link disabled" title="Модуль Proof не активен">
<i class="fa-solid fa-lock text-muted me-2"></i> Proof
</a>
{% endif %}
</div>
</div>
<!-- /SIDEBAR -->
+3 -3
View File
@@ -30,9 +30,9 @@
<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>
<h3>Всего модулей</h3>
<div class="value">{{ stats.total_modules|number_format(0, '', ' ') }}</div>
<div class="icon">📦</div>
</div>
</div>
+2 -1
View File
@@ -71,7 +71,8 @@
<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/modules') }}" class="{{ is_active_route('superadmin/modules') ? 'active' : '' }}">📦 Модули</a></li>
<li><a href="{{ base_url('/superadmin/subscriptions') }}" class="{{ is_active_route('superadmin/subscriptions') ? '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>
+92
View File
@@ -0,0 +1,92 @@
{% extends 'superadmin/layout.twig' %}
{% block title %}Модули - Суперадмин{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Модули системы</h1>
</div>
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="modulesTabs" role="tablist">
{% for code, module in modules %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if loop.first %}active{% endif %}"
id="{{ code }}-tab"
data-bs-toggle="tab"
data-bs-target="#{{ code }}-tab-pane"
type="button"
role="tab"
aria-controls="{{ code }}-tab-pane"
aria-selected="{{ loop.first ? 'true' : 'false' }}">
{{ module.name }}
</button>
</li>
{% endfor %}
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="modulesTabsContent">
{% for code, module in modules %}
<div class="tab-pane fade {% if loop.first %}show active{% endif %}"
id="{{ code }}-tab-pane"
role="tabpanel"
aria-labelledby="{{ code }}-tab"
tabindex="0">
<form action="{{ base_url('/superadmin/modules/update') }}" method="post" class="row g-3">
{{ csrf_field()|raw }}
<input type="hidden" name="module_code" value="{{ code }}">
<div class="col-md-6">
<label class="form-label">Название модуля</label>
<input type="text" name="name" class="form-control" value="{{ module.name }}" required>
</div>
<div class="col-md-6">
<label class="form-label">Описание</label>
<input type="text" name="description" class="form-control" value="{{ module.description }}">
</div>
<div class="col-md-4">
<label class="form-label">Цена (руб/месяц)</label>
<input type="number" name="price_monthly" class="form-control" value="{{ module.price_monthly }}" min="0">
</div>
<div class="col-md-4">
<label class="form-label">Цена (руб/год)</label>
<input type="number" name="price_yearly" class="form-control" value="{{ module.price_yearly }}" min="0">
</div>
<div class="col-md-4">
<label class="form-label">Дней триала</label>
<input type="number" name="trial_days" class="form-control" value="{{ module.trial_days }}" min="0">
</div>
<div class="col-12">
<div class="alert alert-info">
<strong>Код модуля:</strong> {{ code }}<br>
<strong>Возможности:</strong>
<ul class="mb-0 mt-2">
{% for feature in module.features %}
<li>{{ feature }}</li>
{% else %}
<li>Нет описания возможностей</li>
{% endfor %}
</ul>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-save"></i> Сохранить изменения
</button>
</div>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
+213 -188
View File
@@ -1,220 +1,245 @@
{% extends 'superadmin/layout.twig' %}
{% block content %}
<div class="sa-header">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Организация: {{ organization.name }}</h1>
<a href="{{ base_url('/superadmin/organizations') }}" class="btn btn-primary">← Назад</a>
<a href="{{ base_url('/superadmin/organizations') }}" class="btn btn-outline-secondary">
<i class="fa-solid fa-arrow-left"></i> Назад к списку
</a>
</div>
{% for alert in get_alerts() %}
<div class="alert alert-{{ alert.type }}">{{ alert.message }}</div>
<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>
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Подписки на модули</h5>
</div>
<div class="card-body">
{% if subscriptions is empty %}
<p class="text-muted text-center py-4">У организации нет активных подписок</p>
{% else %}
<a href="{{ base_url('/superadmin/organizations/unblock/' ~ organization.id) }}" class="btn btn-success" onclick="return confirm('Разблокировать организацию?')">✅ Разблокировать</a>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Модуль</th>
<th>Статус</th>
<th>Истекает</th>
<th>Создана</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for sub in subscriptions %}
{% set module = allModules[sub.module_code] %}
<tr>
<td>
<strong>{{ module.name|default(sub.module_code) }}</strong>
<div class="text-muted small">{{ module.description|default('') }}</div>
</td>
<td>
{% if sub.status == 'active' %}
<span class="badge bg-success">Активна</span>
{% elseif sub.status == 'trial' %}
<span class="badge bg-info">Триал</span>
{% elseif sub.status == 'expired' %}
<span class="badge bg-danger">Истекла</span>
{% else %}
<span class="badge bg-warning">{{ sub.status }}</span>
{% endif %}
</td>
<td>
{% if sub.expires_at %}
{{ sub.expires_at|date('d.m.Y H:i') }}
{% else %}
<span class="text-muted">Бессрочно</span>
{% endif %}
</td>
<td>{{ sub.created_at|date('d.m.Y H:i') }}</td>
<td>
<a href="{{ base_url('/superadmin/organizations/' ~ organization.id ~ '/removeSubscription/' ~ sub.id) }}"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Удалить подписку на модуль {{ module.name|default(sub.module_code) }}?')"
title="Удалить подписку">
<i class="fa-solid fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Участники организации</h5>
</div>
<div class="card-body">
{% if users is empty %}
<p class="text-muted text-center py-4">Участников пока нет</p>
{% else %}
<div class="table-responsive">
<table class="table table-hover">
<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' ? 'bg-danger' : (user.role == 'admin' ? 'bg-warning' : 'bg-info') }}">
{{ user.role }}
</span>
</td>
<td>
{% if user.status == 'active' %}
<span class="badge bg-success">Активен</span>
{% elseif user.status == 'blocked' %}
<span class="badge bg-danger">Заблокирован</span>
{% else %}
<span class="badge bg-warning">{{ user.status }}</span>
{% endif %}
</td>
<td>{{ user.created_at|date('d.m.Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% 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 }}
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Добавить подписку</h5>
</div>
<div class="card-body">
<form action="{{ base_url('/superadmin/organizations/' ~ organization.id ~ '/add-subscription') }}" method="post">
{{ csrf_field()|raw }}
<div class="mb-3">
<label class="form-label">Модуль</label>
<select name="module_code" class="form-select" required>
<option value="">Выберите модуль...</option>
{% for code, module in allModules %}
{% set hasSub = false %}
{% for sub in subscriptions %}
{% if sub.module_code == code %}
{% set hasSub = true %}
{% endif %}
{% endfor %}
<option value="{{ code }}" {{ hasSub ? 'disabled style="background-color: #f8f9fa;"' : '' }}>
{{ module.name }} - {{ module.price_monthly }} руб/мес {{ hasSub ? '(уже есть)' : '' }}
</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 }}
{% endfor %}
</select>
</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 class="mb-3">
<label class="form-label">Срок действия (дней)</label>
<input type="number" name="duration_days" class="form-control" value="30" min="0">
<div class="form-text">0 - бессрочно</div>
</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 class="mb-3">
<label class="form-label">Статус</label>
<select name="status" class="form-select">
<option value="active">Активна</option>
<option value="trial">Триал</option>
<option value="expired">Истекла</option>
<option value="cancelled">Отменена</option>
</select>
</div>
<div style="color: #7f8c8d; font-size: 12px;">Менеджеры</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fa-solid fa-plus"></i> Добавить подписку
</button>
</form>
</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>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Информация</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>Пользователь</th>
<th>Email</th>
<th>Роль</th>
<th>Статус</th>
<th>Дата добавления</th>
<td>ID</td>
<td class="text-end">{{ organization.id }}</td>
</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>
<td>Тип</td>
<td class="text-end">
{% if organization.type == 'business' %}
<span class="badge bg-info">Бизнес</span>
{% else %}
<span class="badge badge-warning">{{ user.status }}</span>
<span class="badge bg-warning">Личное</span>
{% endif %}
</td>
<td>{{ user.created_at|date('d.m.Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<tr>
<td>Статус</td>
<td class="text-end">
{% if organization.status == 'active' %}
<span class="badge bg-success">Активна</span>
{% elseif organization.status == 'blocked' %}
<span class="badge bg-danger">Заблокирована</span>
{% else %}
<span class="badge bg-warning">{{ organization.status }}</span>
{% endif %}
</td>
</tr>
<tr>
<td>Создана</td>
<td class="text-end">{{ organization.created_at|date('d.m.Y H:i') }}</td>
</tr>
<tr>
<td>Участников</td>
<td class="text-end">{{ users|length }}</td>
</tr>
</table>
</div>
</div>
<div class="d-flex flex-column gap-2">
{% if organization.status == 'active' %}
<a href="{{ base_url('/superadmin/organizations/block/' ~ organization.id) }}"
class="btn btn-warning"
onclick="return confirm('Заблокировать организацию?')">
<i class="fa-solid fa-ban"></i> Заблокировать
</a>
{% else %}
<a href="{{ base_url('/superadmin/organizations/unblock/' ~ organization.id) }}"
class="btn btn-success"
onclick="return confirm('Разблокировать организацию?')">
<i class="fa-solid fa-check"></i> Разблокировать
</a>
{% endif %}
<a href="{{ base_url('/superadmin/organizations/delete/' ~ organization.id) }}"
class="btn btn-danger"
onclick="return confirm('Удалить организацию? Это действие нельзя отменить!')">
<i class="fa-solid fa-trash"></i> Удалить организацию
</a>
</div>
</div>
</div>
{% endblock %}
-95
View File
@@ -1,95 +0,0 @@
{% 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
@@ -1,96 +0,0 @@
{% 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
@@ -1,77 +0,0 @@
{% 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,209 @@
{% extends 'superadmin/layout.twig' %}
{% block title %}Добавить подписку - Суперадмин{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Добавить подписку</h1>
<a href="{{ base_url('/superadmin/subscriptions') }}" class="btn btn-outline-secondary">
<i class="fa-solid fa-arrow-left"></i> Назад к списку
</a>
</div>
<div class="card">
<div class="card-body">
<form action="{{ base_url('/superadmin/subscriptions/store') }}" method="post" class="row g-3">
{{ csrf_field()|raw }}
<div class="col-md-6">
<label class="form-label">Организация *</label>
<div class="autocomplete-wrapper">
<input type="text" class="form-control autocomplete-input" placeholder="Начните вводить название организации..."
data-url="{{ base_url('/superadmin/organizations/search') }}" autocomplete="off">
<input type="hidden" name="organization_id" class="autocomplete-value" value="">
<div class="autocomplete-dropdown"></div>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Модуль *</label>
<select name="module_code" class="form-select" required>
<option value="">Выберите модуль...</option>
{% for code, module in modules %}
<option value="{{ code }}">{{ module.name }} - {{ module.price_monthly }} руб/мес</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Количество дней</label>
<input type="number" name="duration_days" class="form-control" value="30" min="0" placeholder="0 - бессрочно">
<div class="form-text">0 - подписка без срока истечения</div>
</div>
<div class="col-md-4">
<label class="form-label">Статус</label>
<select name="status" class="form-select">
<option value="active">Активна</option>
<option value="trial">Триал</option>
<option value="expired">Истекла</option>
<option value="cancelled">Отменена</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Истекает</label>
<input type="text" class="form-control" disabled value="Будет рассчитано автоматически">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-plus"></i> Создать подписку
</button>
</div>
</form>
</div>
</div>
<style>
.autocomplete-wrapper {
position: relative;
}
.autocomplete-input {
width: 100%;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 5px 5px;
max-height: 250px;
overflow-y: auto;
display: none;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.autocomplete-dropdown.active {
display: block;
}
.autocomplete-item {
padding: 12px 15px;
cursor: pointer;
border-bottom: 1px solid #eee;
transition: background-color 0.2s;
}
.autocomplete-item:hover,
.autocomplete-item.selected {
background-color: #f8f9fa;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-empty {
padding: 12px 15px;
color: #6c757d;
text-align: center;
}
</style>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.querySelector('.autocomplete-input');
const hiddenInput = document.querySelector('.autocomplete-value');
const dropdown = document.querySelector('.autocomplete-dropdown');
let timeout = null;
input.addEventListener('input', function() {
clearTimeout(timeout);
const value = this.value.trim();
if (value.length < 2) {
dropdown.classList.remove('active');
hiddenInput.value = '';
return;
}
timeout = setTimeout(() => {
fetch(this.dataset.url + '?q=' + encodeURIComponent(value))
.then(response => response.json())
.then(data => {
dropdown.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(item => {
const div = document.createElement('div');
div.className = 'autocomplete-item';
div.textContent = item.text;
div.dataset.id = item.id;
div.addEventListener('click', function() {
input.value = this.textContent;
hiddenInput.value = this.dataset.id;
dropdown.classList.remove('active');
});
dropdown.appendChild(div);
});
} else {
const div = document.createElement('div');
div.className = 'autocomplete-empty';
div.textContent = 'Организации не найдены';
dropdown.appendChild(div);
}
dropdown.classList.add('active');
})
.catch(error => {
console.error('Error:', error);
});
}, 300);
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.autocomplete-wrapper')) {
dropdown.classList.remove('active');
}
});
input.addEventListener('focus', function() {
if (this.value.trim().length >= 2) {
this.dispatchEvent(new Event('input'));
}
});
input.addEventListener('keydown', function(e) {
const items = dropdown.querySelectorAll('.autocomplete-item');
const selected = dropdown.querySelector('.selected');
let index = Array.from(items).indexOf(selected);
if (e.key === 'ArrowDown') {
e.preventDefault();
if (items.length > 0) {
if (index < items.length - 1) {
if (selected) selected.classList.remove('selected');
items[index + 1].classList.add('selected');
}
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (index > 0) {
if (selected) selected.classList.remove('selected');
items[index - 1].classList.add('selected');
}
} else if (e.key === 'Enter' && selected) {
e.preventDefault();
input.value = selected.textContent;
hiddenInput.value = selected.dataset.id;
dropdown.classList.remove('active');
} else if (e.key === 'Escape') {
dropdown.classList.remove('active');
}
});
});
</script>
{% endblock %}
@@ -0,0 +1,63 @@
{% extends 'superadmin/layout.twig' %}
{% block title %}Подписки - Суперадмин{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Управление подписками</h1>
<a href="{{ base_url('/superadmin/subscriptions/create') }}" class="btn btn-primary">
<i class="fa-solid fa-plus"></i> Добавить подписку
</a>
</div>
{% if session.success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session.success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if session.error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session.error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="card">
<div class="card-body">
{{ tableHtml|raw }}
{# CSRF токен для AJAX запросов #}
{{ csrf_field()|raw }}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ site_url('/assets/js/modules/DataTable.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
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 %}