first commit
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
CLI::error('ERROR: ' . $code);
|
||||
CLI::write($message);
|
||||
CLI::newLine();
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')) ?> —
|
||||
PHP: <?= esc(PHP_VERSION) ?> —
|
||||
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 →</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 →</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'])) : ?>
|
||||
— <?= 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'])) : ?>
|
||||
— <?= 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
Reference in New Issue
Block a user