first commit

This commit is contained in:
root
2026-01-10 16:16:39 +03:00
commit 5f5a28e292
154 changed files with 21454 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
{# app/Modules/Clients/Views/_table.twig #}
{# Определяем тип запроса: AJAX = только tbody + footer #}
{% set isAjax = app.request.headers.get('X-Requested-With') == 'XMLHttpRequest' %}
{# Настройки пагинации - ИСПОЛЬЗУЕМ pagerDetails напрямую #}
{% if pagerDetails is defined %}
{% set pagination = pagerDetails %}
{% else %}
{# Fallback если pagerDetails нет #}
{% set pagination = {
currentPage: 1,
totalPages: 1,
total: clients|length|default(0),
perPage: perPage|default(10),
from: 1,
to: clients|length|default(0)
} %}
{% endif %}
{# Проверка на пустое состояние #}
{% set isEmpty = clients is empty or clients|length == 0 %}
{# AJAX запрос - tbody + footer #}
<tbody>
{% if isEmpty %}
<tr>
<td colspan="4" class="text-center py-5">
<i class="fa-solid fa-users text-muted mb-3" style="font-size: 3rem;"></i>
<p class="text-muted mb-3">
{% if filters.name or filters.email or filters.phone %}
Клиенты не найдены
{% else %}
Клиентов пока нет
{% endif %}
</p>
<a href="{{ base_url('/clients/new') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-2"></i>Добавить клиента
</a>
</td>
</tr>
{% else %}
{% for client in clients %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-primary text-white rounded-circle d-flex justify-content-center align-items-center me-3" style="width:40px;height:40px;">
{{ client.name|first|upper }}
</div>
<div>
<strong>{{ client.name }}</strong>
{% if client.notes %}
<br><small class="text-muted">{{ client.notes|slice(0, 50) }}{{ client.notes|length > 50 ? '...' : '' }}</small>
{% endif %}
</div>
</div>
</td>
<td>
{% if client.email %}
<a href="mailto:{{ client.email }}">{{ client.email }}</a>
{% else %}
<span class="text-muted">—</span>
{% endif %}
</td>
<td>
{% if client.phone %}
<a href="tel:{{ client.phone }}">{{ client.phone }}</a>
{% else %}
<span class="text-muted">—</span>
{% endif %}
</td>
<td class="text-end">
<a href="{{ base_url('/clients/edit/' ~ client.id) }}" class="btn btn-sm btn-outline-primary" title="Редактировать">
<i class="fa-solid fa-pen"></i>
</a>
<a href="{{ base_url('/clients/delete/' ~ client.id) }}" class="btn btn-sm btn-outline-danger" title="Удалить" onclick="return confirm('Вы уверены что хотите удалить этого клиента?');">
<i class="fa-solid fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
<tfoot>
<tr>
<td colspan="4">
{{ include('@components/table/pagination.twig', { pagination: pagination, id: 'clients-table' }) }}
</td>
</tr>
</tfoot>
+73
View File
@@ -0,0 +1,73 @@
{% extends 'layouts/base.twig' %}
{% import 'macros/forms.twig' as forms %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex align-items-center">
<a href="{{ base_url('/clients') }}" class="btn btn-outline-secondary me-3">
<i class="fa-solid fa-arrow-left"></i>
</a>
<div>
<h1 class="h4 mb-0">{{ title }}</h1>
</div>
</div>
</div>
<div class="card-body">
{{ forms.form_open(client ? base_url('/clients/update/' ~ client.id) : base_url('/clients/create')) }}
<div class="mb-3">
<label for="name" class="form-label fw-bold">Имя / Название *</label>
<input type="text" name="name" id="name" class="form-control {{ errors.name ? 'is-invalid' : '' }}"
value="{{ old.name ?? client.name ?? '' }}" required autofocus>
{% if errors.name %}
<div class="invalid-feedback">{{ errors.name }}</div>
{% endif %}
<div class="form-text">ФИО клиента или название компании</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" id="email" class="form-control {{ errors.email ? 'is-invalid' : '' }}"
value="{{ old.email ?? client.email ?? '' }}">
{% if errors.email %}
<div class="invalid-feedback">{{ errors.email }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Телефон</label>
<input type="tel" name="phone" id="phone" class="form-control {{ errors.phone ? 'is-invalid' : '' }}"
value="{{ old.phone ?? client.phone ?? '' }}">
{% if errors.phone %}
<div class="invalid-feedback">{{ errors.phone }}</div>
{% endif %}
</div>
</div>
<div class="mb-4">
<label for="notes" class="form-label">Заметки</label>
<textarea name="notes" id="notes" rows="4" class="form-control {{ errors.notes ? 'is-invalid' : '' }}"
placeholder="Дополнительная информация о клиенте...">{{ old.notes ?? client.notes ?? '' }}</textarea>
{% if errors.notes %}
<div class="invalid-feedback">{{ errors.notes }}</div>
{% endif %}
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ base_url('/clients') }}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-check me-2"></i>
{{ client ? 'Сохранить изменения' : 'Добавить клиента' }}
</button>
</div>
{{ forms.form_close() }}
</div>
</div>
</div>
</div>
{% endblock %}
+115
View File
@@ -0,0 +1,115 @@
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">Клиенты</h1>
<p class="text-muted mb-0">Управление клиентами вашей организации</p>
</div>
<a href="{{ base_url('/clients/new') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-2"></i>Добавить клиента
</a>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex align-items-center justify-content-between">
<div class="text-muted small">
Нажмите на <i class="fa-solid fa-search text-muted"></i> для поиска по столбцу
</div>
</div>
</div>
{# Формируем строки таблицы из клиентов #}
{% set tableRows = [] %}
{% if clients is defined and clients|length > 0 %}
{% for client in clients %}
{% set tableRows = tableRows|merge([{
cells: [
{
content: '<div class="d-flex align-items-center">
<div class="bg-primary text-white rounded-circle d-flex justify-content-center align-items-center me-3" style="width:40px;height:40px;">' ~ client.name|first|upper ~ '</div>
<div><strong>' ~ client.name ~ '</strong>' ~ (client.notes ? '<br><small class="text-muted">' ~ client.notes|slice(0, 50) ~ (client.notes|length > 50 ? '...' : '') ~ '</small>') ~ '</div>
</div>',
class: ''
},
{
content: client.email ? '<a href="mailto:' ~ client.email ~ '">' ~ client.email ~ '</a>' : '<span class="text-muted">—</span>',
class: ''
},
{
content: client.phone ? '<a href="tel:' ~ client.phone ~ '">' ~ client.phone ~ '</a>' : '<span class="text-muted">—</span>',
class: ''
}
],
actions: '<a href="' ~ base_url('/clients/edit/' ~ client.id) ~ '" class="btn btn-sm btn-outline-primary" title="Редактировать"><i class="fa-solid fa-pen"></i></a>
<a href="' ~ base_url('/clients/delete/' ~ client.id) ~ '" class="btn btn-sm btn-outline-danger" title="Удалить" onclick="return confirm(\'Вы уверены что хотите удалить этого клиента?\');"><i class="fa-solid fa-trash"></i></a>'
}]) %}
{% endfor %}
{% endif %}
<div id="clients-table">
{{ include('@components/table/table.twig', {
id: 'clients-table',
url: '/clients/table',
perPage: perPage|default(10),
sort: sort|default(''),
order: order|default('asc'),
filters: filters|default({}),
columns: {
name: { label: 'Имя / Название', width: '40%' },
email: { label: 'Email', width: '25%' },
phone: { label: 'Телефон', width: '20%' }
},
rows: tableRows,
pagerDetails: {
currentPage: pagerDetails.currentPage|default(1),
pageCount: pagerDetails.pageCount|default(1),
total: pagerDetails.total|default(0),
perPage: perPage|default(10),
from: pagerDetails.from|default(1),
to: pagerDetails.to|default(clients|length|default(0))
},
actions: { label: 'Действия', width: '15%' },
emptyMessage: 'Клиентов пока нет',
emptyIcon: 'fa-solid fa-users',
emptyActionUrl: base_url('/clients/new'),
emptyActionLabel: 'Добавить клиента',
emptyActionIcon: 'fa-solid fa-plus'
}) }}
{# CSRF токен для AJAX запросов #}
{{ csrf_field()|raw }}
</div>
</div>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
{% endblock %}
{% block scripts %}
{{ parent() }}
<script src="/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 %}