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
+32
View File
@@ -0,0 +1,32 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="card shadow-sm" style="width: 100%; max-width: 400px;">
<div class="card-body p-4">
<div class="text-center mb-4">
<i class="fa-solid fa-circle-nodes fa-3x text-primary mb-2"></i>
<h3>Бизнес.Точка</h3>
<p class="text-muted">Вход в систему</p>
</div>
{{ form_open(base_url('/login'), 'class="needs-validation"') }}
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" value="{{ old.email|default('') }}" class="form-control" id="email" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" name="password" value="{{ old.password|default('') }}" class="form-control" id="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Войти</button>
{{ form_close() }}
<div class="mt-3 text-center">
<small>Нет аккаунта? <a href="{{ base_url('/register') }}">Зарегистрироваться</a></small>
</div>
</div>
</div>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="card shadow-sm" style="width: 100%; max-width: 400px;">
<div class="card-body p-4">
<h3 class="text-center mb-4">Регистрация</h3>
{# ИСПОЛЬЗУЕМ МАКРОС form_open. CSRF добавится АВТОМАТИЧЕСКИ #}
{{ form_open(base_url('/register'), 'class="needs-validation"') }}
<div class="mb-3">
<label for="name" class="form-label">Ваше имя</label>
<input type="text" name="name" class="form-control" id="name" required value="{{ old.name|default('') }}">
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" class="form-control" id="email" required value="{{ old.email|default('') }}">
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" name="password" class="form-control" id="password" required minlength="6">
</div>
<button type="submit" class="btn btn-primary w-100">Создать аккаунт</button>
{# ЗАКРЫВАЕМ ФОРМУ МАКРОСОМ #}
{{ form_close() }}
<div class="mt-3 text-center">
<small>Уже есть аккаунт? <a href="{{ base_url('/login') }}">Войти</a></small>
</div>
</div>
</div>
{% endblock %}
+41
View File
@@ -0,0 +1,41 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center" style="min-height: 80vh;">
<div class="card shadow-lg border-0" style="max-width: 500px; width: 100%;">
<div class="card-body p-5 text-center">
<div class="mb-4">
<i class="fa-solid fa-envelope-circle-check display-1 text-primary"></i>
</div>
<h3 class="mb-3">Регистрация успешна!</h3>
<p class="text-muted mb-4">
Мы отправили письмо с ссылкой для подтверждения email на вашу почту.
</p>
<div class="alert alert-info text-start">
<i class="fa-solid fa-info-circle me-2"></i>
<strong>Что делать дальше:</strong>
<ol class="mb-0 mt-2">
<li>Откройте почтовый ящик</li>
<li>Найдите письмо от нас</li>
<li>Нажмите на ссылку для подтверждения</li>
</ol>
</div>
<div class="mb-4">
<a href="/auth/resend-verification" class="text-muted small">
<i class="fa-solid fa-rotate-right me-1"></i>Не получили письмо? Отправить снова
</a>
</div>
<hr class="my-4">
<a href="/" class="btn btn-outline-secondary">
<i class="fa-solid fa-home me-2"></i>На главную
</a>
</div>
</div>
</div>
{% endblock %}
+41
View File
@@ -0,0 +1,41 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center" style="min-height: 80vh;">
<div class="card shadow-lg border-0" style="max-width: 500px; width: 100%;">
<div class="card-body p-5">
<div class="text-center mb-4">
<i class="fa-solid fa-envelope-open-text display-4 text-primary"></i>
<h4 class="mt-3">Повторная отправка письма</h4>
<p class="text-muted">Введите ваш email для получения новой ссылки подтверждения</p>
</div>
{% from 'macros/forms.twig' import form_open, form_close %}
{{ form_open(base_url('/auth/resend-verification')) }}
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" class="form-control {{ errors.email is defined ? 'is-invalid' : '' }}"
id="email" required value="{{ old.email|default('') }}"
placeholder="name@example.com">
{% if errors.email is defined %}
<div class="invalid-feedback">{{ errors.email }}</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-paper-plane me-2"></i>Отправить письмо
</button>
<a href="/" class="btn btn-outline-secondary">
На главную
</a>
</div>
{{ form_close() }}
</div>
</div>
</div>
{% endblock %}
+36
View File
@@ -0,0 +1,36 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center" style="min-height: 80vh;">
<div class="card shadow-lg border-0" style="max-width: 500px; width: 100%;">
<div class="card-body p-5 text-center">
<div class="mb-4">
<i class="fa-solid fa-triangle-exclamation display-1 text-warning"></i>
</div>
<h3 class="mb-3">Ошибка подтверждения</h3>
<div class="alert alert-warning text-start">
{{ message|default('Недействительная ссылка для подтверждения.') }}
</div>
<div class="mb-4">
<a href="/auth/resend-verification" class="btn btn-outline-primary">
<i class="fa-solid fa-envelope me-2"></i>Запросить ссылку повторно
</a>
</div>
<hr class="my-4">
<div class="d-flex justify-content-center gap-3">
<a href="/" class="btn btn-outline-secondary">
<i class="fa-solid fa-home me-2"></i>На главную
</a>
<a href="/login" class="btn btn-outline-secondary">
<i class="fa-solid fa-sign-in-alt me-2"></i>Войти
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends 'layouts/public.twig' %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center" style="min-height: 80vh;">
<div class="card shadow-lg border-0" style="max-width: 500px; width: 100%;">
<div class="card-body p-5 text-center">
<div class="mb-4">
<i class="fa-solid fa-circle-check display-1 text-success"></i>
</div>
<h3 class="mb-3">Email подтверждён!</h3>
<p class="text-muted mb-4">
{% if name %}
{{ name }}, спасибо за подтверждение.
{% else %}
Спасибо за подтверждение.
{% endif %}
</p>
<p class="text-muted mb-4">
Теперь вы можете войти в систему и начать работу.
</p>
<div class="d-grid gap-2">
<a href="/login" class="btn btn-primary btn-lg">
<i class="fa-solid fa-sign-in-alt me-2"></i>Войти
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
{# app/Views/components/alerts.twig #}
{% set alerts = get_alerts() %}
{% if alerts is not empty %}
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1050">
{% for alert in alerts %}
{# Преобразуем наш тип 'error' в класс Bootstrap 'danger' #}
{% set bs_type = alert.type == 'error' ? 'danger' : alert.type %}
<div class="toast align-items-center text-white bg-{{ bs_type }} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
{{ alert.message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
{% endfor %}
</div>
<script>
// Автоматически показываем все toasts при загрузке страницы
document.addEventListener("DOMContentLoaded", function() {
var toastElList = [].slice.call(document.querySelectorAll('.toast'));
toastElList.forEach(function(toastEl) {
var toast = new bootstrap.Toast(toastEl);
toast.show();
});
});
</script>
{% endif %}
+275
View File
@@ -0,0 +1,275 @@
# DataTable Components
Переиспользуемые компоненты для отображения интерактивных таблиц с AJAX-загрузкой, сортировкой и поиском.
## Структура компонентов
```
public/
├── js/
│ └── modules/
│ └── DataTable.js # JS-модуль для инициализации таблиц
└── css/
└── components/
└── data-table.css # Стили для интерактивных таблиц
app/Views/components/table/
├── table.twig # Основной компонент таблицы
├── table_header.twig # Переиспользуемый заголовок
└── pagination.twig # Компонент пагинации
```
## Быстрый старт
### 1. Подключение стилей и скриптов
В вашем шаблоне добавьте:
```twig
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="/css/components/data-table.css">
{% endblock %}
{% block scripts %}
{{ parent() }}
<script src="/js/modules/DataTable.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
new DataTable('your-table-id', {
url: '/your-module/table',
perPage: 10
});
});
</script>
{% endblock %}
```
### 2. Использование компонента таблицы
```twig
{{ include('@components/table/table.twig', {
id: 'products-table',
url: '/products/table',
perPage: 25,
sort: sort|default(''),
order: order|default('asc'),
filters: filters|default({}),
columns: [
{ name: 'name', label: 'Название', width: '35%' },
{ name: 'sku', label: 'Артикул', width: '15%' },
{ name: 'price', label: 'Цена', width: '15%' },
{ name: 'stock', label: 'Остаток', width: '15%' }
],
actions: { label: 'Действия', width: '20%' },
emptyMessage: 'Товары не найдены'
}) }}
```
## Конфигурация столбцов
Каждый столбец поддерживает следующие параметры:
| Параметр | Тип | Описание |
|----------|-----|----------|
| `name` | string | Идентификатор столбца (используется для сортировки и фильтрации) |
| `label` | string | Отображаемое название столбца |
| `width` | string | Ширина столбца (например, '35%', '200px') |
| `placeholder` | string | Текст-подсказка в поле поиска |
| `searchTitle` | string | Title для иконки поиска |
| `align` | string | CSS-класс выравнивания |
## Конфигурация пагинации
Компонент автоматически получает данные из объекта `pager`:
```php
// В контроллере
$pagination = [
'currentPage' => $pager->getCurrentPage(),
'totalPages' => $pager->getPageCount(),
'totalRecords' => $pager->getTotal(),
'perPage' => $perPage,
'from' => (($pager->getCurrentPage() - 1) * $perPage + 1),
'to' => min($pager->getCurrentPage() * $perPage, $pager->getTotal())
];
```
## Пример контроллера
```php
public function table()
{
$page = (int) ($this->request->getGet('page') ?? 1);
$perPage = (int) ($this->request->getGet('perPage') ?? 10);
$sort = $this->request->getGet('sort') ?? '';
$order = $this->request->getGet('order') ?? 'asc';
// Фильтры
$filters = [
'name' => $this->request->getGet('filters[name]') ?? '',
];
// Построение запроса
$builder = $this->model->builder();
// Применяем фильтры
if (!empty($filters['name'])) {
$builder->like('name', $filters['name']);
}
// Сортировка
if (!empty($sort)) {
$builder->orderBy($sort, $order);
}
// Пагинация
$items = $builder->paginate($perPage, 'default', $page);
$data = [
'items' => $items,
'pager' => $this->model->pager,
'perPage' => $perPage,
'sort' => $sort,
'order' => $order,
'filters' => $filters,
];
return $this->renderTwig('path/to/your/_table', $data);
}
```
## AJAX-ответ
Для AJAX-запросов контроллер должен возвращать только `tbody` и `tfoot`:
```twig
{# _table.twig для модуля #}
{% set isAjax = app.request.headers.get('X-Requested-With') == 'XMLHttpRequest' %}
{% if isAjax %}
{# AJAX: только tbody #}
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td class="text-end">
<a href="...">Редактировать</a>
</td>
</tr>
{% endfor %}
</tbody>
{% if items is not empty and pager %}
<tfoot>
<tr>
<td colspan="3">
{{ include('@components/table/pagination.twig', {
pagination: paginationData,
id: 'your-table-id'
}) }}
</td>
</tr>
</tfoot>
{% endif %}
{% else %}
{# Обычный запрос: полная таблица #}
<div class="table-responsive">
{{ include('@components/table/table.twig', {
id: 'your-table-id',
url: '/your-module/table',
perPage: perPage,
columns: columns,
pagination: paginationData,
actions: { label: 'Действия' },
emptyMessage: 'Нет данных'
}) }}
</div>
{% endif %}
```
## API DataTable
### Опции при инициализации
```javascript
new DataTable('container-id', {
url: '/api/endpoint', // URL для AJAX-загрузки
perPage: 10, // Записей на странице по умолчанию
debounceTime: 300, // Задержка поиска в мс
preserveSearchOnSort: true // Сохранять видимость поиска при сортировке
});
```
### Методы
```javascript
const table = new DataTable('my-table', options);
// Установка фильтра
table.setFilter('columnName', 'value');
// Установка количества записей
table.setPerPage(25);
// Переход на страницу
table.goToPage(3);
```
## Доступные CSS-классы
| Класс | Описание |
|-------|----------|
| `.data-table` | Основной контейнер таблицы |
| `.header-content` | Контейнер для элементов заголовка |
| `.header-text` | Текст заголовка столбца |
| `.search-trigger` | Иконка поиска |
| `.sort-icon` | Иконка сортировки |
| `.header-search-input` | Поле ввода поиска |
| `.sort-icon.active` | Активная сортировка |
| `.pagination-wrapper` | Обёртка пагинации |
## Расширение функциональности
### Добавление кастомных действий
Для добавления кнопок действий в строки:
```twig
{% for client in clients %}
<tr>
<td>{{ client.name }}</td>
<td>{{ client.email }}</td>
<td class="actions-cell text-end">
<a href="{{ editUrl }}" class="btn btn-sm btn-outline-primary">
<i class="fa-solid fa-pen"></i>
</a>
</td>
</tr>
{% endfor %}
```
### Кастомные строки
Компонент поддерживает произвольное содержимое ячеек через параметр `rows`:
```twig
{% set rows = [] %}
{% for product in products %}
{% set rows = rows|merge([{
cells: [
{ content: '<strong>' ~ product.name ~ '</strong>', class: '' },
{ content: product.price ~ ' ₽', class: 'text-end' }
],
actions: '<a href="...">Редактировать</a>'
}]) %}
{% endfor %}
{{ include('@components/table/table.twig', {
id: 'products-table',
rows: rows,
columns: columns,
...
}) }}
```
@@ -0,0 +1,95 @@
{#
pagination.twig - Универсальный компонент пагинации
Использует встроенный пейджер CodeIgniter 4
Параметры:
- pagination: Объект с данными пагинации (из pager->getDetails())
- currentPage: Текущая страница
- pageCount: Всего страниц
- total: Всего записей
- perPage: Записей на странице
- from: Начальная запись
- to: Конечная запись
- id: ID таблицы для уникальности элементов
#}
{% set currentPage = pagination.currentPage|default(1) %}
{% set totalPages = pagination.pageCount|default(1) %}
{% set totalRecords = pagination.total|default(0) %}
{% set perPage = pagination.perPage|default(10) %}
{% set from = pagination.from|default((currentPage - 1) * perPage + 1) %}
{% set to = pagination.to|default(min(currentPage * perPage, totalRecords)) %}
{# Информация о записях #}
{% set infoText = 'Показано ' ~ from ~ '–' ~ to ~ ' из ' ~ totalRecords %}
<div class="pagination-wrapper">
{# Информация о количестве записей #}
<div class="pagination-info">
<span>{{ infoText }}</span>
</div>
{# Кнопки навигации - посередине #}
<nav aria-label="Пагинация" style="float: left;">
<ul class="pagination pagination-sm mb-0">
{# Предыдущая страница #}
<li class="page-item {{ currentPage <= 1 ? 'disabled' }}">
<a class="page-link" href="#" data-nav-page="prev" aria-label="Предыдущая">
<i class="fa-solid fa-chevron-left"></i>
</a>
</li>
{# Номера страниц #}
{% set startPage = max(1, currentPage - 2) %}
{% set endPage = min(totalPages, currentPage + 2) %}
{# Всегда показываем первую страницу если есть #}
{% if startPage > 1 %}
<li class="page-item">
<a class="page-link" href="#" data-page="1">1</a>
</li>
{% if startPage > 2 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endif %}
{# Страницы вокруг текущей #}
{% for page in startPage..endPage %}
<li class="page-item {{ page == currentPage ? 'active' }}">
<a class="page-link" href="#" data-page="{{ page }}">{{ page }}</a>
</li>
{% endfor %}
{# Всегда показываем последнюю страницу если есть #}
{% if endPage < totalPages %}
{% if endPage < totalPages - 1 %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
<li class="page-item">
<a class="page-link" href="#" data-page="{{ totalPages }}">{{ totalPages }}</a>
</li>
{% endif %}
{# Следующая страница #}
<li class="page-item {{ currentPage >= totalPages ? 'disabled' }}">
<a class="page-link" href="#" data-nav-page="next" aria-label="Следующая">
<i class="fa-solid fa-chevron-right"></i>
</a>
</li>
</ul>
</nav>
{# Выбор количества записей - справа #}
<div class="per-page-selector" style="float: right;">
<label for="per-page-select-{{ id|default('table') }}">Записей на странице:</label>
<select id="per-page-select-{{ id|default('table') }}" class="form-select-sm per-page-select">
<option value="10" {{ perPage == 10 ? 'selected' }}>10</option>
<option value="25" {{ perPage == 25 ? 'selected' }}>25</option>
<option value="50" {{ perPage == 50 ? 'selected' }}>50</option>
<option value="100" {{ perPage == 100 ? 'selected' }}>100</option>
</select>
</div>
</div>
+104
View File
@@ -0,0 +1,104 @@
{#
table.twig - Универсальный компонент таблицы с AJAX-загрузкой
Параметры:
- id: ID контейнера таблицы (обязательно)
- url: URL для AJAX-загрузки данных (обязательно)
- perPage: Количество записей на странице (по умолчанию 10)
- columns: Ассоциативный массив ['name' => ['label' => 'Name', 'width' => '40%']]
- sort: Текущий столбец сортировки
- order: Направление сортировки
- filters: Текущие значения фильтров
- items: Массив объектов модели (автоматический рендеринг)
- rows: Предварительно построенные строки (устаревший формат, для совместимости)
- emptyMessage: Сообщение при отсутствии данных
- emptyActionUrl: URL для кнопки действия (опционально)
- emptyActionLabel: Текст кнопки действия (опционально)
- emptyIcon: FontAwesome иконка (опционально)
- tableClass: Дополнительные классы для таблицы
#}
{% set hasRows = (rows is defined and rows|length > 0) or (items is defined and items|length > 0) %}
<div id="{{ id }}" class="data-table" data-url="{{ url }}" data-per-page="{{ perPage|default(10) }}">
<table class="table table-hover mb-0 {{ tableClass|default('') }}">
{# Заголовок таблицы #}
{{ include('@components/table/table_header.twig', {
columns: columns,
sort: sort|default(''),
order: order|default('asc'),
filters: filters|default({}),
actions: actions|default(false)
}) }}
{# Тело таблицы #}
<tbody>
{% if hasRows %}
{% if rows is defined and rows|length > 0 %}
{# Старый формат: предварительно построенные строки #}
{% for row in rows %}
<tr>
{% for cell in row.cells %}
<td class="{{ cell.class|default('') }}">{{ cell.content|raw }}</td>
{% endfor %}
{% if row.actions is defined %}
<td class="actions-cell text-end">
{{ row.actions|raw }}
</td>
{% endif %}
</tr>
{% endfor %}
{% elseif items is defined and items|length > 0 %}
{# Новый формат: автоматический рендеринг из объектов модели #}
{% set columnKeys = columns|keys %}
{% for item in items %}
<tr>
{# Ячейки данных #}
{% for columnKey in columnKeys %}
<td>{{ attribute(item, columnKey)|default('—') }}</td>
{% endfor %}
{# Колонка действий #}
{% if actions is defined and actions %}
<td class="actions-cell text-end">
{% if item.actions is defined %}{{ item.actions|raw }}{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
{% endif %}
{% else %}
{# Пустое состояние #}
<tr>
<td colspan="{{ columns|length + (actions is defined and actions ? 1 : 0) }}"
class="text-center py-5">
{% if emptyIcon is defined and emptyIcon %}
<div class="mb-3">
<i class="{{ emptyIcon }} text-muted" style="font-size: 3rem;"></i>
</div>
{% endif %}
<p class="text-muted mb-3">{{ emptyMessage|default('Нет данных для отображения') }}</p>
{% if emptyActionUrl is defined and emptyActionUrl %}
<a href="{{ emptyActionUrl }}" class="btn btn-primary">
{% if emptyActionIcon is defined and emptyActionIcon %}
<i class="{{ emptyActionIcon }} me-2"></i>
{% endif %}
{{ emptyActionLabel|default('Добавить') }}
</a>
{% endif %}
</td>
</tr>
{% endif %}
</tbody>
<tfoot>
<tr>
<td colspan="{{ columns|length + 1 }}">
{{ include('@components/table/pagination.twig', {
pagination: pagerDetails,
id: id
}) }}
</td>
</tr>
</tfoot>
</table>
</div>
@@ -0,0 +1,51 @@
{#
table_header.twig - Переиспользуемый шаблон заголовка таблицы
Параметры:
- columns: Ассоциативный массив столбцов ['name' => ['label' => 'Name', 'width' => '40%']]
- sort: Текущий столбец сортировки
- order: Направление сортировки (asc/desc)
- filters: Текущие значения фильтров
#}
<thead class="table-light">
<tr>
{% for columnKey, column in columns %}
<th style="width: {{ column.width|default('auto') }};"
class="{{ column.align|default('') }}"
data-sort-column="{{ columnKey }}">
<div class="header-content">
{# Поле поиска - первым, для правильного позиционирования #}
{# Иконка поиска #}
<i class="fa-solid fa-search search-trigger text-muted"
data-search-trigger="{{ columnKey }}"
title="{{ column.searchTitle|default('Поиск по ' ~ column.label|lower) }}"></i>
<input type="text"
class="form-control-sm header-search-input"
data-search-input="{{ columnKey }}"
value="{{ filters[columnKey]|default('') }}"
placeholder="{{ column.placeholder|default('Поиск...') }}"
style="display: none;">
{# Текст заголовка #}
<span class="header-text" data-header-text="{{ columnKey }}">
{{ column.label }}
</span>
{# Иконка сортировки #}
<i class="fa-solid fa-sort sort-icon {{ sort == columnKey ? 'active' : 'text-muted' }}"
data-sort="{{ columnKey }}"
title="{{ sort == columnKey ? (order == 'asc' ? 'Сортировка по возрастанию (нажмите для сортировки по убыванию)' : 'Сортировка по убыванию (нажмите для сортировки по возрастанию)') : 'Сортировка' }}"></i>
</div>
</th>
{% endfor %}
{# Колонка действий (опционально) #}
{% if actions is defined and actions %}
<th class="text-end {{ actions.class|default('') }}" style="width: {{ actions.width|default('auto') }};">
{{ actions.label|default('Действия') }}
</th>
{% endif %}
</tr>
</thead>
+61
View File
@@ -0,0 +1,61 @@
{# app/Views/dashboard/index.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2>{% if current_org %}Добро пожаловать в {{ current_org.name }}!{% else %}Добро пожаловать!{% endif %}</h2>
<p class="text-muted">Ваш личный кабинет для управления бизнесом.</p>
</div>
</div>
<div class="row">
<!-- Карточки модулей (плейсхолдеры) -->
<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>
<p class="card-text text-muted">Управление клиентами</p>
<a href="#" 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-calendar-check fa-3x text-success mb-3"></i>
<h5 class="card-title">Booking</h5>
<p class="card-text text-muted">Запись на приём</p>
<a href="#" class="btn btn-outline-success 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-file-signature fa-3x text-warning mb-3"></i>
<h5 class="card-title">Proof</h5>
<p class="card-text text-muted">Согласование файлов</p>
<a href="#" class="btn btn-outline-warning 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-list-check fa-3x text-danger mb-3"></i>
<h5 class="card-title">Tasks</h5>
<p class="card-text text-muted">Задачи и проекты</p>
<a href="#" class="btn btn-outline-danger btn-sm">Скоро</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Подтверждение регистрации</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.logo {
text-align: center;
margin-bottom: 20px;
font-size: 24px;
font-weight: bold;
color: #0d6efd;
}
.btn {
display: inline-block;
padding: 12px 24px;
background-color: #0d6efd;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
}
.btn:hover {
background-color: #0b5ed7;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">{{ app_name }}</div>
<h2>Добрый день, {{ name }}!</h2>
<p>Спасибо за регистрацию в {{ app_name }}.</p>
<p>Для завершения регистрации и подтверждения вашего email адреса, пожалуйста, нажмите на кнопку ниже:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{ verification_url }}" class="btn">Подтвердить email</a>
</p>
<p>Если кнопка не работает, вы можете скопировать ссылку и вставить её в адресную строку браузера:</p>
<p style="word-break: break-all; font-size: 12px; color: #666; background: #f8f9fa; padding: 10px; border-radius: 4px;">
{{ verification_url }}
</p>
<p>Ссылка действительна в течение 24 часов.</p>
<p>Если вы не регистрировались на {{ app_name }}, просто проигнорируйте это письмо.</p>
</div>
<div class="footer">
<p>© {{ "now"|date("Y") }} {{ app_name }}. Все права защищены.</p>
</div>
</div>
</body>
</html>
+85
View File
@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Добро пожаловать</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.card {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.logo {
text-align: center;
margin-bottom: 20px;
font-size: 24px;
font-weight: bold;
color: #198754;
}
.btn {
display: inline-block;
padding: 12px 24px;
background-color: #198754;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
text-align: center;
}
.btn:hover {
background-color: #157347;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">{{ app_name }}</div>
<h2>Добро пожаловать, {{ name }}!</h2>
<p>Поздравляем! Ваш email успешно подтверждён.</p>
<p>Теперь вы можете:</p>
<ul>
<li>Создавать и управлять организациями</li>
<li>Приглашать сотрудников</li>
<li>Использовать все функции платформы</li>
</ul>
<p style="text-align: center; margin: 30px 0;">
<a href="{{ base_url('/') }}" class="btn">Перейти в личный кабинет</a>
</p>
<p>Если у вас возникнут вопросы, наша служба поддержки всегда готова помочь.</p>
<p>С уважением,<br>Команда {{ app_name }}</p>
</div>
<div class="footer">
<p>© {{ "now"|date("Y") }} {{ app_name }}. Все права защищены.</p>
</div>
</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<?php
use CodeIgniter\CLI\CLI;
CLI::error('ERROR: ' . $code);
CLI::write($message);
CLI::newLine();
+65
View File
@@ -0,0 +1,65 @@
<?php
use CodeIgniter\CLI\CLI;
// The main Exception
CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
CLI::write($message);
CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green'));
CLI::newLine();
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
CLI::write(' Caused by:');
CLI::write(' [' . $prevException::class . ']', 'red');
CLI::write(' ' . $prevException->getMessage());
CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
CLI::newLine();
}
// The backtrace
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$backtraces = $last->getTrace();
if ($backtraces) {
CLI::write('Backtrace:', 'green');
}
foreach ($backtraces as $i => $error) {
$padFile = ' '; // 4 spaces
$padClass = ' '; // 7 spaces
$c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
if (isset($error['file'])) {
$filepath = clean_path($error['file']) . ':' . $error['line'];
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
} else {
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
}
$function = '';
if (isset($error['class'])) {
$type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
$function .= $padClass . $error['class'] . $type . $error['function'];
} elseif (! isset($error['class']) && isset($error['function'])) {
$function .= $padClass . $error['function'];
}
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));
$function .= '(' . $args . ')';
CLI::write($function);
CLI::newLine();
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
// On the CLI, we still want errors in productions
// so just use the exception template.
include __DIR__ . '/error_exception.php';
+194
View File
@@ -0,0 +1,194 @@
:root {
--main-bg-color: #fff;
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #DC4814;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
body {
height: 100%;
background: var(--main-bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--main-text-color);
font-weight: 300;
margin: 0;
padding: 0;
}
h1 {
font-weight: lighter;
font-size: 3rem;
color: var(--dark-text-color);
margin: 0;
}
h1.headline {
margin-top: 20%;
font-size: 5rem;
}
.text-center {
text-align: center;
}
p.lead {
font-size: 1.6rem;
}
.container {
max-width: 75rem;
margin: 0 auto;
padding: 1rem;
}
.header {
background: var(--light-bg-color);
color: var(--dark-text-color);
margin-top: 2.17rem;
}
.header .container {
padding: 1rem;
}
.header h1 {
font-size: 2.5rem;
font-weight: 500;
}
.header p {
font-size: 1.2rem;
margin: 0;
line-height: 2.5;
}
.header a {
color: var(--brand-primary-color);
margin-left: 2rem;
display: none;
text-decoration: none;
}
.header:hover a {
display: inline;
}
.environment {
background: var(--brand-primary-color);
color: var(--main-bg-color);
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
top: 0;
position: fixed;
}
.source {
background: #343434;
color: var(--light-text-color);
padding: 0.5em 1em;
border-radius: 5px;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.85rem;
margin: 0;
overflow-x: scroll;
}
.source span.line {
line-height: 1.4;
}
.source span.line .number {
color: #666;
}
.source .line .highlight {
display: block;
background: var(--dark-text-color);
color: var(--light-text-color);
}
.source span.highlight .number {
color: #fff;
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
padding: 0;
margin-bottom: -1px;
}
.tabs li {
display: inline;
}
.tabs a:link,
.tabs a:visited {
padding: 0 1rem;
line-height: 2.7;
text-decoration: none;
color: var(--dark-text-color);
background: var(--light-bg-color);
border: 1px solid rgba(0,0,0,0.15);
border-bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: inline-block;
}
.tabs a:hover {
background: var(--light-bg-color);
border-color: rgba(0,0,0,0.15);
}
.tabs a.active {
background: var(--main-bg-color);
color: var(--main-text-color);
}
.tab-content {
background: var(--main-bg-color);
border: 1px solid rgba(0,0,0,0.15);
}
.content {
padding: 1rem;
}
.hide {
display: none;
}
.alert {
margin-top: 2rem;
display: block;
text-align: center;
line-height: 3.0;
background: #d9edf7;
border: 1px solid #bcdff1;
border-radius: 5px;
color: #31708f;
}
table {
width: 100%;
overflow: hidden;
}
th {
text-align: left;
border-bottom: 1px solid #e7e7e7;
padding-bottom: 0.5rem;
}
td {
padding: 0.2rem 0.5rem 0.2rem 0;
}
tr:hover td {
background: #f1f1f1;
}
td pre {
white-space: pre-wrap;
}
.trace a {
color: inherit;
}
.trace table {
width: auto;
}
.trace tr td:first-child {
min-width: 5em;
font-weight: bold;
}
.trace td {
background: var(--light-bg-color);
padding: 0 1rem;
}
.trace td pre {
margin: 0;
}
.args {
display: none;
}
+116
View File
@@ -0,0 +1,116 @@
var tabLinks = new Array();
var contentDivs = new Array();
function init()
{
// Grab the tab links and content divs from the page
var tabListItems = document.getElementById('tabs').childNodes;
console.log(tabListItems);
for (var i = 0; i < tabListItems.length; i ++)
{
if (tabListItems[i].nodeName == "LI")
{
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
var id = getHash(tabLink.getAttribute('href'));
tabLinks[id] = tabLink;
contentDivs[id] = document.getElementById(id);
}
}
// Assign onclick events to the tab links, and
// highlight the first tab
var i = 0;
for (var id in tabLinks)
{
tabLinks[id].onclick = showTab;
tabLinks[id].onfocus = function () {
this.blur()
};
if (i == 0)
{
tabLinks[id].className = 'active';
}
i ++;
}
// Hide all content divs except the first
var i = 0;
for (var id in contentDivs)
{
if (i != 0)
{
console.log(contentDivs[id]);
contentDivs[id].className = 'content hide';
}
i ++;
}
}
function showTab()
{
var selectedId = getHash(this.getAttribute('href'));
// Highlight the selected tab, and dim all others.
// Also show the selected content div, and hide all others.
for (var id in contentDivs)
{
if (id == selectedId)
{
tabLinks[id].className = 'active';
contentDivs[id].className = 'content';
}
else
{
tabLinks[id].className = '';
contentDivs[id].className = 'content hide';
}
}
// Stop the browser following the link
return false;
}
function getFirstChildWithTagName(element, tagName)
{
for (var i = 0; i < element.childNodes.length; i ++)
{
if (element.childNodes[i].nodeName == tagName)
{
return element.childNodes[i];
}
}
}
function getHash(url)
{
var hashPos = url.lastIndexOf('#');
return url.substring(hashPos + 1);
}
function toggle(elem)
{
elem = document.getElementById(elem);
if (elem.style && elem.style['display'])
{
// Only works with the "style" attr
var disp = elem.style['display'];
}
else if (elem.currentStyle)
{
// For MSIE, naturally
var disp = elem.currentStyle['display'];
}
else if (window.getComputedStyle)
{
// For most other browsers
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
}
// Toggle the state of the "display" style
elem.style.display = disp == 'block' ? 'none' : 'block';
return false;
}
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.badRequest') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.pageNotFound') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>404</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryCannotFind') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>
+429
View File
@@ -0,0 +1,429 @@
<?php
use CodeIgniter\HTTP\Header;
use CodeIgniter\CodeIgniter;
$errorId = uniqid('error', true);
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<script>
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:s')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
</div>
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
</div>
</div>
<!-- Source -->
<div class="container">
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
<div class="container">
<?php
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php
}
?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<tbody>
<tr>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<?php $empty = false; ?>
<h3>$<?= esc($var) ?></h3>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<!-- Memory -->
<div class="content" id="memory">
<table>
<tbody>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /tab-content -->
</div> <!-- /container -->
<?php endif; ?>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= lang('Errors.whoops') ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
</head>
<body>
<div class="container text-center">
<h1 class="headline"><?= lang('Errors.whoops') ?></h1>
<p class="lead"><?= lang('Errors.weHitASnag') ?></p>
</div>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
{# app/Views/Landing/index.twig #}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Бизнес.Точка - Автоматизация для бизнеса</title>
<link href="{{ base_url('assets/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ base_url('assets/css/all.min.css') }}" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<nav class="d-flex justify-content-between align-items-center py-3">
<h3 class="text-primary fw-bold m-0"><i class="fa-solid fa-circle-nodes"></i> Бизнес.Точка</h3>
<div>
<a href="{{ base_url('/login') }}" class="btn btn-outline-primary me-2">Войти</a>
<a href="{{ base_url('/register') }}" class="btn btn-primary">Попробовать бесплатно</a>
</div>
</nav>
<div class="text-center py-5 my-5">
<h1 class="display-4 fw-bold mb-3">Всё для вашего бизнеса</h1>
<p class="lead text-muted mb-4">CRM, Запись клиентов, Задачи и Согласование файлов в одном месте.</p>
<div class="d-flex justify-content-center gap-3">
<a href="{{ base_url('/register') }}" class="btn btn-primary btn-lg px-5 shadow">Начать бесплатно</a>
</div>
</div>
</div>
</body>
</html>
+158
View File
@@ -0,0 +1,158 @@
{# app/Views/layouts/base.html.twig #}
{% set session_data = get_session() %}
{% set current_org = get_current_org() %}
<!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>
<!-- Bootstrap CSS -->
<link href="{{ base_url('assets/css/bootstrap.min.css') }}" rel="stylesheet">
<!-- FontAwesome -->
<link href="{{ base_url('assets/css/all.min.css') }}" rel="stylesheet">
<!-- Base -->
<link href="{{ base_url('assets/css/base.css') }}" rel="stylesheet">
{% block styles %}{% endblock %}
</head>
<body class="bg-light">
<div class="d-flex" id="wrapper">
<!-- SIDEBAR -->
<div class="bg-white border-end" id="sidebar-wrapper" style="width: 250px;">
<div class="sidebar-heading bg-primary text-white p-3 d-flex justify-content-between align-items-center">
<span><i class="fa-solid fa-circle-nodes me-2"></i>Бизнес.Точка</span>
</div>
<div class="list-group list-group-flush mt-2">
<!-- Главная -->
<a href="{{ base_url('/') }}"
class="list-group-item list-group-item-action list-group-item-light py-3 sidebar-link
{{ is_active_route(['', 'dashboard', 'home']) ? 'active' : '' }}">
<i class="fa-solid fa-gauge-high me-2"></i> Главная
</a>
<!-- Клиенты -->
<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> Клиенты
</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
</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-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
</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-check-square me-2"></i> Tasks
</a>
</div>
</div>
<!-- /SIDEBAR -->
<!-- Page Content -->
<div id="page-content-wrapper" class="w-100">
<!-- TOPBAR -->
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom px-4 shadow-sm">
<button class="btn btn-light border-0" id="sidebarToggle"><i class="fa-solid fa-bars"></i></button>
<div class="ms-auto d-flex align-items-center">
<!-- ДРОПДАУН ОРГАНИЗАЦИИ -->
<div class="dropdown me-4">
{% if current_org %}
<button class="btn btn-light dropdown-toggle border d-flex align-items-center" type="button" data-bs-toggle="dropdown">
<i class="fa-solid fa-building text-primary me-2"></i>
<span class="fw-bold text-dark">{{ current_org.name }}</span>
<span class="badge bg-light text-secondary ms-2 text-uppercase fs-6">{{ current_org.type == 'personal' ? 'Личное' : 'Бизнес' }}</span>
</button>
{% else %}
<a href="{{ base_url('/organizations') }}" class="btn btn-outline-danger">
<i class="fa-solid fa-exclamation-triangle me-1"></i> Выберите организацию
</a>
{% endif %}
{% if current_org %}
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
{# <li>
<span class="dropdown-header text-muted small">
<i class="fa-solid fa-hashtag me-1"></i> {{ current_org.id }}
</span>
</li>
<li><hr class="dropdown-divider"></li> #}
<!-- Ссылка: Список всех организаций -->
<li>
<a class="dropdown-item" href="{{ base_url('/organizations') }}">
<i class="fa-solid fa-layer-group text-primary me-2"></i> Список организаций
</a>
</li>
<!-- Ссылка: Создать новую организацию -->
<li>
<a class="dropdown-item" href="{{ base_url('/organizations/create') }}">
<i class="fa-solid fa-plus-circle text-success me-2"></i> Создать организацию
</a>
</li>
<li><hr class="dropdown-divider"></li>
<!-- Ссылка: Настройки этой организации (будущий функционал) -->
<li>
<a class="dropdown-item text-muted" href="#">
<i class="fa-solid fa-gear me-2"></i> Настройки текущей
</a>
</li>
</ul>
{% endif %}
</div>
<!-- /ДРОПДАУН ОРГАНИЗАЦИИ -->
<!-- ПРОФИЛЬ ПОЛЬЗОВАТЕЛЯ -->
<div class="dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
<div class="bg-primary text-white rounded-circle d-flex justify-content-center align-items-center me-2" style="width:32px; height:32px;">
{{ session_data.name|first|upper }}
</div>
<span class="d-none d-md-inline text-dark">{{ session_data.name }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
<li><h6 class="dropdown-header">{{ session_data.email }}</h6></li>
<li><a class="dropdown-item" href="#"><i class="fa-regular fa-user me-2"></i> Профиль</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{{ base_url('/logout') }}"><i class="fa-solid fa-arrow-right-from-bracket me-2"></i> Выйти</a></li>
</ul>
</div>
<!-- /ПРОФИЛЬ ПОЛЬЗОВАТЕЛЯ -->
</div>
</nav>
<!-- /TOPBAR -->
<!-- CONTENT -->
<div class="container-fluid p-4">
<!-- Подключаем компонент алертов (Toasts) -->
{% include 'components/alerts.twig' %}
{% block content %}{% endblock %}
</div>
<!-- /CONTENT -->
</div>
</div>
<!-- Scripts -->
<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>
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход - Бизнес.Точка</title>
<link href="{{ base_url('assets/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ base_url('assets/css/all.min.css') }}" rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center justify-content-center" style="height: 100vh;">
{% include 'components/alerts.twig' %}
{% block content %}{% endblock %}
<script src="{{ base_url('assets/js/bootstrap.bundle.min.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
{# app/Views/macros/forms.twig #}
{% macro form_open(action, attributes = '') %}
<form action="{{ action }}" method="post" {{ attributes|raw }}>
{# Выводим глобальную переменную csrf_token #}
{{ csrf_field()|raw }}
{% endmacro %}
{% macro form_close() %}
</form>
{% endmacro %}
+143
View File
@@ -0,0 +1,143 @@
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="container" style="max-width: 800px;">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Создание организации</h5>
<a href="{{ base_url('/organizations') }}" class="btn btn-light btn-sm">
<i class="fa-solid fa-arrow-left"></i> Назад
</a>
</div>
<div class="card-body p-4">
{% from 'macros/forms.twig' import form_open, form_close %}
{{ form_open(base_url('/organizations/create')) }}
{# Основная информация #}
<h6 class="text-muted mb-3">Основная информация</h6>
<div class="mb-3">
<label for="name" class="form-label">Название организации <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control {{ errors.name is defined ? 'is-invalid' : '' }}"
id="name" required value="{{ old.name|default('') }}"
placeholder="Например, ООО Ромашка">
{% if errors.name is defined %}
<div class="invalid-feedback">{{ errors.name }}</div>
{% endif %}
</div>
<hr class="my-4">
{# Регистрационные данные #}
<h6 class="text-muted mb-3">Регистрационные данные</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label for="inn" class="form-label">ИНН</label>
<input type="text" name="inn" class="form-control" id="inn"
value="{{ old.inn|default('') }}"
placeholder="10 или 12 цифр" maxlength="12">
</div>
<div class="col-md-4 mb-3">
<label for="ogrn" class="form-label">ОГРН</label>
<input type="text" name="ogrn" class="form-control" id="ogrn"
value="{{ old.ogrn|default('') }}"
placeholder="13 цифр" maxlength="13">
</div>
<div class="col-md-4 mb-3">
<label for="kpp" class="form-label">КПП</label>
<input type="text" name="kpp" class="form-control" id="kpp"
value="{{ old.kpp|default('') }}"
placeholder="9 цифр" maxlength="9">
</div>
</div>
<hr class="my-4">
{# Адреса #}
<h6 class="text-muted mb-3">Адреса</h6>
<div class="mb-3">
<label for="legal_address" class="form-label">Юридический адрес</label>
<input type="text" name="legal_address" class="form-control" id="legal_address"
value="{{ old.legal_address|default('') }}"
placeholder="г. Москва, ул. Примерная, д. 1">
</div>
<div class="mb-3">
<label for="actual_address" class="form-label">Фактический адрес</label>
<input type="text" name="actual_address" class="form-control" id="actual_address"
value="{{ old.actual_address|default('') }}"
placeholder="г. Москва, ул. Примерная, д. 1">
<div class="form-text">Если совпадает с юридическим — оставьте пустым</div>
</div>
<hr class="my-4">
{# Контакты #}
<h6 class="text-muted mb-3">Контакты</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label for="phone" class="form-label">Телефон</label>
<input type="tel" name="phone" class="form-control" id="phone"
value="{{ old.phone|default('') }}"
placeholder="+7 (999) 123-45-67">
</div>
<div class="col-md-4 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" class="form-control" id="email"
value="{{ old.email|default('') }}"
placeholder="info@company.ru">
</div>
<div class="col-md-4 mb-3">
<label for="website" class="form-label">Веб-сайт</label>
<input type="url" name="website" class="form-control" id="website"
value="{{ old.website|default('') }}"
placeholder="https://company.ru">
</div>
</div>
<hr class="my-4">
{# Банковские реквизиты #}
<h6 class="text-muted mb-3">Банковские реквизиты</h6>
<div class="mb-3">
<label for="bank_name" class="form-label">Название банка</label>
<input type="text" name="bank_name" class="form-control" id="bank_name"
value="{{ old.bank_name|default('') }}"
placeholder="ПАО Сбербанк г. Москва">
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="bank_bik" class="form-label">БИК</label>
<input type="text" name="bank_bik" class="form-control" id="bank_bik"
value="{{ old.bank_bik|default('') }}"
placeholder="9 цифр" maxlength="9">
</div>
<div class="col-md-4 mb-3">
<label for="checking_account" class="form-label">Расчётный счёт</label>
<input type="text" name="checking_account" class="form-control" id="checking_account"
value="{{ old.checking_account|default('') }}"
placeholder="20 цифр" maxlength="20">
</div>
<div class="col-md-4 mb-3">
<label for="correspondent_account" class="form-label">Корреспондентский счёт</label>
<input type="text" name="correspondent_account" class="form-control" id="correspondent_account"
value="{{ old.correspondent_account|default('') }}"
placeholder="20 цифр" maxlength="20">
</div>
</div>
<div class="d-grid gap-2 d-md-flex mt-4">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fa-solid fa-check me-2"></i>Создать организацию
</button>
<a href="{{ base_url('/organizations') }}" class="btn btn-outline-secondary btn-lg">
Отмена
</a>
</div>
{{ form_close() }}
</div>
</div>
</div>
{% endblock %}
+50
View File
@@ -0,0 +1,50 @@
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="container" style="max-width: 500px; margin-top: 100px;">
<div class="card shadow border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0"><i class="fa-solid fa-triangle-exclamation me-2"></i>Подтверждение удаления</h5>
</div>
<div class="card-body text-center p-4">
<p class="lead">Вы уверены, что хотите удалить организацию?</p>
<div class="alert alert-warning">
<h5 class="alert-heading">{{ organization.name }}</h5>
{% if organization.inn %}
<p class="mb-0">ИНН: {{ organization.inn }}</p>
{% endif %}
{% if organization.type == 'personal' %}
<span class="badge bg-info">Личное пространство</span>
{% else %}
<span class="badge bg-secondary">Организация</span>
{% endif %}
</div>
<div class="alert alert-danger">
<strong>Внимание!</strong> Это действие нельзя отменить.<br>
Будут удалены:
<ul class="text-start mb-0 mt-2">
<li>Все данные организации</li>
<li>Все связи с пользователями</li>
<li>Все записи, связанные с организацией</li>
</ul>
</div>
{% from 'macros/forms.twig' import form_open, form_close %}
{{ form_open(base_url('/organizations/delete/' ~ organization.id)) }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-danger btn-lg">
<i class="fa-solid fa-trash me-2"></i>Да, удалить безвозвратно
</button>
<a href="{{ base_url('/organizations/edit/' ~ organization.id) }}" class="btn btn-outline-secondary">
Нет, отмена
</a>
</div>
{{ form_close() }}
</div>
</div>
</div>
{% endblock %}
+147
View File
@@ -0,0 +1,147 @@
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="container" style="max-width: 800px;">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Редактирование организации</h5>
<a href="{{ base_url('/organizations') }}" class="btn btn-light btn-sm">
<i class="fa-solid fa-arrow-left"></i> Назад
</a>
</div>
<div class="card-body p-4">
{% from 'macros/forms.twig' import form_open, form_close %}
{{ form_open(base_url('/organizations/edit/' ~ organization.id)) }}
{# Основная информация #}
<h6 class="text-muted mb-3">Основная информация</h6>
<div class="mb-3">
<label for="name" class="form-label">Название организации <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control {{ errors.name is defined ? 'is-invalid' : '' }}"
id="name" required value="{{ old.name|default(organization.name) }}"
placeholder="Например, ООО Ромашка">
{% if errors.name is defined %}
<div class="invalid-feedback">{{ errors.name }}</div>
{% endif %}
</div>
<hr class="my-4">
{# Регистрационные данные #}
<h6 class="text-muted mb-3">Регистрационные данные</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label for="inn" class="form-label">ИНН</label>
<input type="text" name="inn" class="form-control" id="inn"
value="{{ old.inn|default(requisites.inn) }}"
placeholder="10 или 12 цифр" maxlength="12">
</div>
<div class="col-md-4 mb-3">
<label for="ogrn" class="form-label">ОГРН</label>
<input type="text" name="ogrn" class="form-control" id="ogrn"
value="{{ old.ogrn|default(requisites.ogrn) }}"
placeholder="13 цифр" maxlength="13">
</div>
<div class="col-md-4 mb-3">
<label for="kpp" class="form-label">КПП</label>
<input type="text" name="kpp" class="form-control" id="kpp"
value="{{ old.kpp|default(requisites.kpp) }}"
placeholder="9 цифр" maxlength="9">
</div>
</div>
<hr class="my-4">
{# Адреса #}
<h6 class="text-muted mb-3">Адреса</h6>
<div class="mb-3">
<label for="legal_address" class="form-label">Юридический адрес</label>
<input type="text" name="legal_address" class="form-control" id="legal_address"
value="{{ old.legal_address|default(requisites.legal_address) }}"
placeholder="г. Москва, ул. Примерная, д. 1">
</div>
<div class="mb-3">
<label for="actual_address" class="form-label">Фактический адрес</label>
<input type="text" name="actual_address" class="form-control" id="actual_address"
value="{{ old.actual_address|default(requisites.actual_address) }}"
placeholder="г. Москва, ул. Примерная, д. 1">
<div class="form-text">Если совпадает с юридическим — оставьте пустым</div>
</div>
<hr class="my-4">
{# Контакты #}
<h6 class="text-muted mb-3">Контакты</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label for="phone" class="form-label">Телефон</label>
<input type="tel" name="phone" class="form-control" id="phone"
value="{{ old.phone|default(requisites.phone) }}"
placeholder="+7 (999) 123-45-67">
</div>
<div class="col-md-4 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" class="form-control" id="email"
value="{{ old.email|default(requisites.email) }}"
placeholder="info@company.ru">
</div>
<div class="col-md-4 mb-3">
<label for="website" class="form-label">Веб-сайт</label>
<input type="url" name="website" class="form-control" id="website"
value="{{ old.website|default(requisites.website) }}"
placeholder="https://company.ru">
</div>
</div>
<hr class="my-4">
{# Банковские реквизиты #}
<h6 class="text-muted mb-3">Банковские реквизиты</h6>
<div class="mb-3">
<label for="bank_name" class="form-label">Название банка</label>
<input type="text" name="bank_name" class="form-control" id="bank_name"
value="{{ old.bank_name|default(requisites.bank_name) }}"
placeholder="ПАО Сбербанк г. Москва">
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="bank_bik" class="form-label">БИК</label>
<input type="text" name="bank_bik" class="form-control" id="bank_bik"
value="{{ old.bank_bik|default(requisites.bank_bik) }}"
placeholder="9 цифр" maxlength="9">
</div>
<div class="col-md-4 mb-3">
<label for="checking_account" class="form-label">Расчётный счёт</label>
<input type="text" name="checking_account" class="form-control" id="checking_account"
value="{{ old.checking_account|default(requisites.checking_account) }}"
placeholder="20 цифр" maxlength="20">
</div>
<div class="col-md-4 mb-3">
<label for="correspondent_account" class="form-label">Корреспондентский счёт</label>
<input type="text" name="correspondent_account" class="form-control" id="correspondent_account"
value="{{ old.correspondent_account|default(requisites.correspondent_account) }}"
placeholder="20 цифр" maxlength="20">
</div>
</div>
<div class="d-grid gap-2 d-md-flex mt-4">
<button type="submit" class="btn btn-success btn-lg">
<i class="fa-solid fa-check me-2"></i>Сохранить изменения
</button>
<a href="{{ base_url('/organizations') }}" class="btn btn-outline-secondary btn-lg">
Отмена
</a>
<a href="{{ base_url('/organizations/delete/' ~ organization.id) }}"
class="btn btn-outline-danger btn-lg ms-md-auto">
<i class="fa-solid fa-trash me-2"></i>Удалить организацию
</a>
</div>
{{ form_close() }}
</div>
</div>
</div>
{% endblock %}
+108
View File
@@ -0,0 +1,108 @@
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="container" style="max-width: 900px;">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Мои организации</h4>
<a href="{{ base_url('/organizations/create') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-2"></i>Создать организацию
</a>
</div>
{% if organizations is empty %}
<div class="card shadow-sm">
<div class="card-body text-center py-5">
<i class="fa-solid fa-building display-1 text-muted mb-3"></i>
<h5 class="text-muted">Организаций пока нет</h5>
<p class="text-muted">Создайте свою первую организацию для начала работы</p>
<a href="{{ base_url('/organizations/create') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-2"></i>Создать организацию
</a>
</div>
</div>
{% else %}
<div class="row">
{% for org in organizations %}
{# Декодируем JSON реквизиты #}
{% set req = org.requisites ? org.requisites|json_decode : {} %}
<div class="col-md-6 mb-3">
<div class="card shadow-sm h-100 {{ org.id == session.get('active_org_id') ? 'border-primary' : '' }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="card-title mb-1">
{% if org.type == 'personal' %}
<i class="fa-solid fa-user text-info me-2"></i>
{% else %}
<i class="fa-solid fa-building text-primary me-2"></i>
{% endif %}
{{ org.name }}
</h5>
<small class="text-muted">
{% if org.type == 'personal' %}
Личное пространство
{% else %}
{% if req.inn %}ИНН: {{ req.inn }}{% endif %}
{% endif %}
</small>
</div>
{% if org.id == session.get('active_org_id') %}
<span class="badge bg-primary">Активна</span>
{% endif %}
</div>
{# Краткая информация #}
{% if req.phone or req.email %}
<div class="mb-3">
{% if req.phone %}
<small class="d-block text-muted">
<i class="fa-solid fa-phone me-1"></i>{{ req.phone }}
</small>
{% endif %}
{% if req.email %}
<small class="d-block text-muted">
<i class="fa-solid fa-envelope me-1"></i>{{ req.email }}
</small>
{% endif %}
</div>
{% endif %}
<div class="d-flex flex-wrap gap-2">
{# Кнопка выбора/переключения #}
{% if org.id != session.get('active_org_id') %}
<a href="{{ base_url('/organizations/switch/' ~ org.id) }}"
class="btn btn-outline-primary btn-sm">
<i class="fa-solid fa-check me-1"></i>Выбрать
</a>
{% endif %}
{# Кнопка редактирования #}
<a href="{{ base_url('/organizations/edit/' ~ org.id) }}"
class="btn btn-outline-secondary btn-sm">
<i class="fa-solid fa-pen me-1"></i>Ред.
</a>
{# Кнопка удаления (только для бизнес-организаций) #}
{% if org.type == 'business' %}
<a href="{{ base_url('/organizations/delete/' ~ org.id) }}"
class="btn btn-outline-danger btn-sm">
<i class="fa-solid fa-trash me-1"></i>Удалить
</a>
{% endif %}
</div>
</div>
<div class="card-footer bg-transparent text-muted small">
Создана: {{ org.created_at|date('d.m.Y') }}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{# Статистика #}
<div class="mt-4 text-center text-muted small">
Всего организаций: {{ count }}
</div>
</div>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
<?php
use CodeIgniter\Pager\PagerRenderer;
/**
* @var PagerRenderer $pager
*/
$pager->setSurroundCount(2);
?>
<nav aria-label="<?= lang('Pager.pageNavigation') ?>">
<ul class="pagination justify-content-center">
<?php if ($pager->hasPrevious()) : ?>
<li class="page-item">
<a href="<?= $pager->getPrevious() ?>" class="page-link" aria-label="<?= lang('Pager.previous') ?>">
<span aria-hidden="true"><?= lang('Pager.previous') ?></span>
</a>
</li>
<?php endif ?>
<?php foreach ($pager->links() as $link) : ?>
<li class="page-item <?= $link['active'] ? 'active' : '' ?>">
<a href="<?= $link['uri'] ?>" class="page-link"><?= $link['title'] ?></a>
</li>
<?php endforeach ?>
<?php if ($pager->hasNext()) : ?>
<li class="page-item">
<a href="<?= $pager->getNext() ?>" class="page-link" aria-label="<?= lang('Pager.next') ?>">
<span aria-hidden="true"><?= lang('Pager.next') ?></span>
</a>
</li>
<?php endif ?>
</ul>
</nav>
File diff suppressed because one or more lines are too long