add CRM module start
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
{#
|
||||
calendar.twig - Универсальный компонент календаря
|
||||
|
||||
Параметры:
|
||||
- events: Массив событий
|
||||
Пример:
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Событие 1',
|
||||
date: '2026-01-15',
|
||||
color: '#3B82F6',
|
||||
url: '/path/to/event'
|
||||
}
|
||||
]
|
||||
- currentMonth: Текущий месяц в формате YYYY-MM
|
||||
- prevMonth: URL или параметр для предыдущего месяца
|
||||
- nextMonth: URL или параметр для следующего месяца
|
||||
- eventComponent: Имя Twig компонента для рендеринга событий (опционально)
|
||||
- onEventClick: JavaScript функция при клике на событие (опционально)
|
||||
- showLegend: Показывать легенду (опционально, по умолчанию true)
|
||||
- legend: Массив для легенды (опционально)
|
||||
Пример:
|
||||
legend: [
|
||||
{ name: 'Этап 1', color: '#3B82F6' }
|
||||
]
|
||||
#}
|
||||
{# Навигация по месяцам #}
|
||||
{% if showNavigation|default(true) %}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<a href="{{ prevMonth }}" class="btn btn-outline-secondary">
|
||||
<i class="fa-solid fa-chevron-left me-1"></i>Предыдущий
|
||||
</a>
|
||||
<h5 class="mb-0">{{ monthName }}</h5>
|
||||
<a href="{{ nextMonth }}" class="btn btn-outline-secondary">
|
||||
Следующий<i class="fa-solid fa-chevron-right ms-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Календарь #}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="calendar">
|
||||
{# Дни недели #}
|
||||
<div class="calendar-header bg-light">
|
||||
{% for day in ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] %}
|
||||
<div class="calendar-header-cell text-center py-2 text-muted small fw-normal">
|
||||
{{ day }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Сетка календаря #}
|
||||
<div class="calendar-grid">
|
||||
{# Пустые ячейки до первого дня #}
|
||||
{% for i in 0..(firstDayOfWeek - 1) %}
|
||||
<div class="calendar-cell bg-light"></div>
|
||||
{% endfor %}
|
||||
|
||||
{# Дни месяца #}
|
||||
{% for day in 1..daysInMonth %}
|
||||
{% set dateStr = currentMonth ~ '-' ~ (day < 10 ? '0' ~ day : day) %}
|
||||
{% set dayEvents = eventsByDate[dateStr]|default([]) %}
|
||||
{% set isToday = dateStr == today %}
|
||||
|
||||
<div class="calendar-cell {{ isToday ? 'calendar-cell-today' : '' }}">
|
||||
<div class="calendar-day-number {{ isToday ? 'text-primary fw-bold' : '' }}">
|
||||
{{ day }}
|
||||
</div>
|
||||
|
||||
<div class="calendar-events">
|
||||
{% for event in dayEvents|slice(0, 3) %}
|
||||
{% if eventComponent is defined %}
|
||||
{{ include(eventComponent, {event: event}) }}
|
||||
{% else %}
|
||||
{{ include('@components/calendar/default_event.twig', {event: event, onEventClick: onEventClick|default('')}) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if dayEvents|length > 3 %}
|
||||
<div class="calendar-events-more text-muted small">
|
||||
+{{ dayEvents|length - 3 }} ещё
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# Пустые ячейки после последнего дня #}
|
||||
{% set remainingCells = 7 - ((firstDayOfWeek + daysInMonth) % 7) %}
|
||||
{% if remainingCells < 7 %}
|
||||
{% for i in 1..remainingCells %}
|
||||
<div class="calendar-cell bg-light"></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Легенда #}
|
||||
{% if showLegend|default(true) and (legend is defined or events is defined) %}
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Легенда</h6>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if legend is defined %}
|
||||
{% for item in legend %}
|
||||
<span class="badge"
|
||||
style="background-color: {{ item.color }}20; color: {{ item.color }}; border: 1px solid {{ item.color }}40;">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{# Автоматическая легенда из типов событий #}
|
||||
{% set uniqueColors = {} %}
|
||||
{% for event in events %}
|
||||
{% if event.color is defined and event.color not in uniqueColors %}
|
||||
<span class="badge"
|
||||
style="background-color: {{ event.color }}20; color: {{ event.color }}; border: 1px solid {{ event.color }}40;">
|
||||
{{ event.title }}
|
||||
</span>
|
||||
{% set uniqueColors = uniqueColors|merge([event.color]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{#
|
||||
default_event.twig - Событие по умолчанию для Календаря
|
||||
|
||||
Параметры:
|
||||
- event: Объект события
|
||||
Ожидаемые поля:
|
||||
- id: Идентификатор
|
||||
- title: Заголовок
|
||||
- date: Дата события (для сравнения с today)
|
||||
- color: Цвет для бордера
|
||||
- url: Ссылка (опционально)
|
||||
- onEventClick: JavaScript функция при клике (опционально)
|
||||
#}
|
||||
{% if event.url %}
|
||||
<a href="{{ event.url }}"
|
||||
class="calendar-event"
|
||||
style="border-left-color: {{ event.color|default('#6B7280') }}"
|
||||
{% if onEventClick %}onclick="{{ onEventClick }}({{ event.id }}); return false;"{% endif %}
|
||||
title="{{ event.title }}">
|
||||
{{ event.title|length > 15 ? event.title|slice(0, 15) ~ '...' : event.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="calendar-event"
|
||||
style="border-left-color: {{ event.color|default('#6B7280') }}"
|
||||
{% if onEventClick %}onclick="{{ onEventClick }}({{ event.id }});"{% endif %}
|
||||
title="{{ event.title }}">
|
||||
{{ event.title|length > 15 ? event.title|slice(0, 15) ~ '...' : event.title }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{#
|
||||
default_card.twig - Карточка по умолчанию для Канбан-компонента
|
||||
|
||||
Параметры:
|
||||
- item: Объект элемента
|
||||
- column: Объект колонки (для доступа к color и т.д.)
|
||||
|
||||
Ожидаемые поля в item:
|
||||
- id: Идентификатор
|
||||
- title: Заголовок
|
||||
- url: Ссылка на просмотр (опционально)
|
||||
- amount: Сумма для отображения (опционально)
|
||||
- date: Дата для отображения (опционально)
|
||||
- assignee: Ответственный (опционально)
|
||||
- status: Статус для цветовой маркировки (опционально)
|
||||
#}
|
||||
<div class="card mb-2 kanban-card"
|
||||
draggable="true"
|
||||
data-item-id="{{ item.id }}"
|
||||
style="cursor: grab;">
|
||||
<div class="card-body py-2 px-3">
|
||||
{# Заголовок и сумма #}
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
{% if item.url %}
|
||||
<a href="{{ item.url }}" class="text-decoration-none">
|
||||
<strong class="text-dark">{{ item.title }}</strong>
|
||||
</a>
|
||||
{% else %}
|
||||
<strong class="text-dark">{{ item.title }}</strong>
|
||||
{% endif %}
|
||||
|
||||
{% if item.amount is defined and item.amount %}
|
||||
<span class="badge bg-light text-dark">
|
||||
₽{{ item.amount|number_format(0, ',', ' ') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Дополнительная информация #}
|
||||
{% if item.description is defined and item.description %}
|
||||
<small class="text-muted d-block mb-2">
|
||||
{{ item.description|length > 50 ? item.description|slice(0, 50) ~ '...' : item.description }}
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
{# Нижняя панель #}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{% if item.assignee is defined and item.assignee %}
|
||||
<small class="text-muted">
|
||||
<i class="fa-solid fa-user-check me-1"></i>
|
||||
{{ item.assignee }}
|
||||
</small>
|
||||
{% else %}
|
||||
<small></small>
|
||||
{% endif %}
|
||||
|
||||
{% if item.date is defined and item.date %}
|
||||
<small class="{{ item.isOverdue is defined and item.isOverdue ? 'text-danger' : 'text-muted' }}">
|
||||
<i class="fa-regular fa-calendar me-1"></i>
|
||||
{{ item.date|date('d.m') }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Теги/метки #}
|
||||
{% if item.tags is defined and item.tags|length > 0 %}
|
||||
<div class="mt-2 d-flex flex-wrap gap-1">
|
||||
{% for tag in item.tags %}
|
||||
<span class="badge" style="background-color: {{ tag.color }}20; color: {{ tag.color }}; font-size: 0.65rem;">
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,165 @@
|
||||
{#
|
||||
kanban.twig - Универсальный компонент Канбан-доски
|
||||
|
||||
Параметры:
|
||||
- columns: Массив колонок с данными
|
||||
Пример:
|
||||
columns: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Колонка 1',
|
||||
color: '#3B82F6',
|
||||
items: [...],
|
||||
total: 1000,
|
||||
itemLabel: 'сделка' (опционально, для грамматики)
|
||||
}
|
||||
]
|
||||
- cardComponent: Имя Twig компонента для рендеринга карточек (опционально)
|
||||
- moveUrl: URL для API перемещения элементов (опционально)
|
||||
- onMove: JavaScript функция callback при перемещении (опционально)
|
||||
- emptyMessage: Сообщение при отсутствии элементов (опционально)
|
||||
- addUrl: URL для добавления нового элемента (опционально)
|
||||
- addLabel: Текст кнопки добавления (опционально)
|
||||
#}
|
||||
<div class="kanban-board overflow-auto pb-4">
|
||||
<div class="d-flex gap-3" style="min-width: max-content;">
|
||||
{% for column in columns %}
|
||||
<div class="kanban-column" style="min-width: {{ column.width|default('320px') }}; min-height: 60vh;">
|
||||
{# Заголовок колонки #}
|
||||
<div class="card mb-2"
|
||||
style="border-left: 4px solid {{ column.color }}; border-top: none; border-right: none; border-bottom: none;">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">{{ column.name }}</h6>
|
||||
<span class="badge bg-secondary">{{ column.items|length }}</span>
|
||||
</div>
|
||||
{% if column.total is defined %}
|
||||
<small class="text-muted">
|
||||
₽{{ column.total|number_format(0, ',', ' ') }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Карточки #}
|
||||
<div class="kanban-cards-container"
|
||||
data-column-id="{{ column.id }}"
|
||||
data-move-url="{{ moveUrl|default('') }}"
|
||||
style="min-height: 50vh;"
|
||||
{% if onMove is defined %}data-on-move="{{ onMove }}"{% endif %}>
|
||||
{% if column.items is defined and column.items|length > 0 %}
|
||||
{% for item in column.items %}
|
||||
{% if cardComponent is defined %}
|
||||
{{ include(cardComponent, {item: item, column: column}) }}
|
||||
{% else %}
|
||||
{{ include('@components/kanban/default_card.twig', {item: item, column: column}) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Кнопка добавления #}
|
||||
{% if addUrl is defined or column.addUrl is defined %}
|
||||
<a href="{{ column.addUrl|default(addUrl) }}?column_id={{ column.id }}"
|
||||
class="btn btn-outline-secondary btn-sm w-100 mt-2">
|
||||
<i class="fa-solid fa-plus me-1"></i>
|
||||
{{ addLabel|default('Добавить') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initKanban();
|
||||
});
|
||||
|
||||
function initKanban() {
|
||||
const cards = document.querySelectorAll('.kanban-card[draggable="true"]');
|
||||
const containers = document.querySelectorAll('.kanban-cards-container');
|
||||
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('dragstart', handleDragStart);
|
||||
card.addEventListener('dragend', handleDragEnd);
|
||||
});
|
||||
|
||||
containers.forEach(container => {
|
||||
container.addEventListener('dragover', handleDragOver);
|
||||
container.addEventListener('drop', handleDrop);
|
||||
container.addEventListener('dragenter', handleDragEnter);
|
||||
container.addEventListener('dragleave', handleDragLeave);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
this.classList.add('dragging');
|
||||
e.dataTransfer.setData('text/plain', this.dataset.itemId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
this.classList.remove('dragging');
|
||||
document.querySelectorAll('.kanban-cards-container').forEach(col => {
|
||||
col.classList.remove('bg-light');
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
|
||||
function handleDragEnter(e) {
|
||||
e.preventDefault();
|
||||
this.classList.add('bg-light');
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
this.classList.remove('bg-light');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('bg-light');
|
||||
|
||||
const itemId = e.dataTransfer.getData('text/plain');
|
||||
const newColumnId = this.dataset.columnId;
|
||||
const moveUrl = this.dataset.moveUrl;
|
||||
const onMove = this.dataset.onMove;
|
||||
|
||||
if (itemId && newColumnId) {
|
||||
if (moveUrl) {
|
||||
// AJAX перемещение
|
||||
fetch(moveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: 'item_id=' + itemId + '&column_id=' + newColumnId
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (onMove) {
|
||||
window[onMove](itemId, newColumnId, data);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
alert(data.message || 'Ошибка при перемещении');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при перемещении');
|
||||
});
|
||||
} else if (onMove) {
|
||||
// Только callback без AJAX
|
||||
window[onMove](itemId, newColumnId);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -23,6 +23,13 @@
|
||||
Возможно, оно истекло или было отозвано отправителем.
|
||||
</p>
|
||||
|
||||
{% if expired and expired_at %}
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fa-solid fa-clock me-2"></i>
|
||||
Приглашение истекло {{ expired_at|date("d.m.Y в H:i") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="fa-solid fa-home me-2"></i>На главную
|
||||
|
||||
Reference in New Issue
Block a user