+Интеграция списка задач с календарем
Чтобы интегрировать список задач из модуля "Менеджер задач" в OpenCart 4.1.0.3 с календарем, я добавлю возможность отображения задач с датами выполнения (due_date) в виде событий в календаре. Для этого я использую популярную библиотеку FullCalendar, добавлю новый метод в контроллер для получения задач в формате JSON, и создам отдельную вкладку или страницу в админке для отображения календаря.
---
### 1. Обновление модели (admin/model/extension/module/task_manager.php)
Добавим метод для получения задач с датами выполнения для календаря:
<?php
class ModelExtensionModuleTaskManager extends Model {
public function getTasks($data = []) {
// ... (без изменений)
}
public function getTotalTasks($data = []) {
// ... (без изменений)
}
public function updateTaskOrder($task_id, $sort_order) {
// ... (без изменений)
}
public function getLastModifiedTime() {
// ... (без изменений)
}
public function getUserName($user_id) {
// ... (без изменений)
}
public function getUserGroupName($user_group_id) {
// ... (без изменений)
}
public function getTags() {
// ... (без изменений)
}
public function getCalendarEvents($start_date, $end_date) {
$sql = "SELECT task_id, title, due_date, status, priority FROM " . DB_PREFIX . "task_manager_tasks WHERE due_date IS NOT NULL";
if ($start_date) {
$sql .= " AND due_date >= '" . $this->db->escape($start_date) . "'";
}
if ($end_date) {
$sql .= " AND due_date <= '" . $this->db->escape($end_date) . "'";
}
$sql .= " ORDER BY due_date ASC";
$query = $this->db->query($sql);
return $query->rows;
}
}
---
### 2. Обновление контроллера (admin/controller/extension/module/task_manager.php)
Добавим метод calendar для отображения календаря и метод events для предоставления данных в формате JSON:
`php
<?php
class ControllerExtensionModuleTaskManager extends Controller {
private function logAction($action, $details = '') {
$this->db->query("INSERT INTO `sunscript`.`task_manager_logs` (`user_id`, `action`, `details`) VALUES ('" . (int)$this->user->getId() . "', '" . $this->db->escape($action) . "', '" . $this->db->escape($details) . "')");
}
public function index() {
$this->load->language('extension/module/task_manager');
$this->document->setTitle($this->language->get('heading_title'));
$this->document->addStyle('view/stylesheet/task_manager.css');
$this->document->addScript('view/javascript/task_manager.js');
$this->load->model('extension/module/task_manager');
$this->load->model('user/user_group');
$page = isset($this->request->get['page']) ? (int)$this->request->get['page'] : 1;
if (isset($this->request->get['limit']) && in_array((int)$this->request->get['limit'], [10, 20, 50])) {
$this->session->data['task_manager_limit'] = (int)$this->request->get['limit'];
$this->logAction("Changed limit", "New limit: " . $this->request->get['limit']);
}
$limit = isset($this->session->data['task_manager_limit']) && in_array($this->session->data['task_manager_limit'], [10, 20, 50])
? $this->session->data['task_manager_limit']
: 10;
$filter_title = (isset($this->request->get['filter_title']) && strlen($this->request->get['filter_title']) <= 255)
? $this->request->get['filter_title'] : '';
$filter_status = (isset($this->request->get['filter_status']) && in_array($this->request->get['filter_status'], ['open', 'in_progress', 'completed', '']))
? $this->request->get['filter_status'] : '';
$filter_date_start = (isset($this->request->get['filter_date_start']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['filter_date_start']))
? $this->request->get['filter_date_start'] : '';
$filter_date_end = (isset($this->request->get['filter_date_end']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['filter_date_end']))
? $this->request->get['filter_date_end'] : '';
$filter_priority = (isset($this->request->get['filter_priority']) && in_array($this->request->get['filter_priority'], ['low', 'medium', 'high', '']))
? $this->request->get['filter_priority'] : '';
$filter_due_date_start = (isset($this->request->get['filter_due_date_start']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['filter_due_date_start']))
? $this->request->get['filter_due_date_start'] : '';
$filter_due_date_end = (isset($this->request->get['filter_due_date_end']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['filter_due_date_end']))
? $this->request->get['filter_due_date_end'] : '';
$filter_tag_id = (isset($this->request->get['filter_tag_id']) && is_numeric($this->request->get['filter_tag_id']))
? (int)$this->request->get['filter_tag_id'] : '';
$filter_user_group_id = (isset($this->request->get['filter_user_group_id']) && is_numeric($this->request->get['filter_user_group_id']))
? (int)$this->request->get['filter_user_group_id'] : '';
$sort = (isset($this->request->get['sort']) && in_array($this->request->get['sort'], ['user', 'user_group', 'created_at', 'sort_order']))
? $this->request->get['sort'] : 'sort_order';
$order = (isset($this->request->get['order']) && in_array($this->request->get['order'], ['ASC', 'DESC']))
? $this->request->get['order'] : 'ASC';
$filters = [
'title' => $filter_title,
'status' => $filter_status,
'date_start' => $filter_date_start,
'date_end' => $filter_date_end,
'priority' => $filter_priority,
'due_date_start' => $filter_due_date_start,
'due_date_end' => $filter_due_date_end,
'tag_id' => $filter_tag_id,
'user_group_id' => $filter_user_group_id
];
$this->logAction("Requested tasks", "page={$page}, limit={$limit}, sort={$sort}, order={$order}, filters=" . json_encode($filters));
$filter_data = [
'filter_title' => $filter_title,
'filter_status' => $filter_status,
'filter_date_start' => $filter_date_start,
'filter_date_end' => $filter_date_end,
'filter_priority' => $filter_priority,
'filter_due_date_start' => $filter_due_date_start,
'filter_due_date_end' => $filter_due_date_end,
'filter_tag_id' => $filter_tag_id,
'filter_user_group_id' => $filter_user_group_id,
'sort' => $sort,
'order' => $order,
'start' => ($page - 1) * $limit,
'limit' => $limit
];
$last_modified = $this->model_extension_module_task_manager->getLastModifiedTime();
if (isset($this->request->server['HTTP_IF_MODIFIED_SINCE'])) {
$if_modified_since = strtotime($this->request->server['HTTP_IF_MODIFIED_SINCE']);
if ($last_modified <= $if_modified_since) {
$this->response->addHeader('HTTP/1.1 304 Not Modified');
$this->logAction("Received cached request", "304 Not Modified");
return;
}
}
$tasks = $this->model_extension_module_task_manager->getTasks($filter_data);
$total_tasks = $this->model_extension_module_task_manager->getTotalTasks($filter_data);
$task_data = array_map(function ($task) {
return [
'task_id' => $task['task_id'],
'title' => $task['title'],
'status' => $task['status'],
'priority' => $task['priority'],
'due_date' => $task['due_date'] ? date('Y-m-d', strtotime($task['due_date'])) : '-',
'tags' => $task['tags'] ?? '-',
'user' => $task['user_id'] ? $this->model_extension_module_task_manager->getUserName($task['user_id']) : '-',
'user_group' => $task['user_group_id'] ? $this->model_extension_module_task_manager->getUserGroupName($task['user_group_id']) : '-',
'created_at' => date('Y-m-d', strtotime($task['created_at'])),
'sort_order' => $task['sort_order'],
'edit' => $this->url->link('extension/module/task_manager/edit', 'user_token=' . $this->session->data['user_token'] . '&task_id=' . $task['task_id']),
'delete' => $this->url->link('extension/module/task_manager/delete', 'user_token=' . $this->session->data['user_token'] . '&task_id=' . $task['task_id'])
];
}, $tasks);
$this->response->addHeader('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
$this->response->addHeader('Cache-Control: public, max-age=3600');
if (isset($this->request->server['HTTP_X_REQUESTED_WITH']) && strtolower($this->request->server['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode(['tasks' => $task_data, 'total' => $total_tasks]));
$this->logAction("Sent AJAX response", "Returned {$total_tasks} tasks");
return;
}
$data['breadcrumbs'] = [
['text' => $this->language->get('text_home'), 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'])],
['text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'])],
['text' => $this->language->get('text_calendar'), 'href' => $this->url->link('extension/module/task_manager/calendar', 'user_token=' . $this->session->data['user_token'])]
];
$data['tasks'] = $task_data;
$total_pages = ceil($total_tasks / $limit);
$page = max(1, min($page, $total_pages));
$pagination = new Pagination();
$pagination->total = $total_tasks;
$pagination->page = $page;
$pagination->limit = $limit;
$pagination->url = $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'] . '&filter_title=' . urlencode($filter_title) . '&filter_status=' . $filter_status . '&filter_date_start=' . urlencode($filter_date_start) . '&filter_date_end=' . urlencode($filter_date_end) . '&filter_priority=' . urlencode($filter_priority) . '&filter_due_date_start=' . urlencode($filter_due_date_start) . '&filter_due_date_end=' . urlencode($filter_due_date_end) . '&filter_tag_id=' . urlencode($filter_tag_id) . '&filter_user_group_id=' . urlencode($filter_user_group_id) . '&sort=' . $sort . '&order=' . $order . '&limit=' . $limit . '&page={page}');
$data['pagination'] = $pagination->render();
$data['total_pages'] = $total_pages;
$data['current_page'] = $page;
$data['limit'] = $limit;
$data['limit_options'] = [10, 20, 50];
$data['filter_title'] = $filter_title;
$data['filter_status'] = $filter_status;
$data['filter_date_start'] = $filter_date_start;
$data['filter_date_end'] = $filter_date_end;
$data['filter_priority'] = $filter_priority;
$data['filter_due_date_start'] = $filter_due_date_start;
$data['filter_due_date_end'] = $filter_due_date_end;
$data['filter_tag_id'] = $filter_tag_id;
$data['filter_user_group_id'] = $filter_user_group_id;
$data['sort'] = $sort;
$data['order'] = $order;
$data['sort_user'] = $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'] . '&filter_title=' . urlencode($filter_title) . '&filter_status=' . $filter_status . '&filter_date_start=' . urlencode($filter_date_start) . '&filter_date_end=' . urlencode($filter_date_end) . '&filter_priority=' . urlencode($filter_priority) . '&filter_due_date_start=' . urlencode($filter_due_date_start) . '&filter_due_date_end=' . urlencode($filter_due_date_end) . '&filter_tag_id=' . urlencode($filter_tag_id) . '&filter_user_group_id=' . urlencode($filter_user_group_id) . '&sort=user&order=' . ($sort == 'user' && $order == 'ASC' ? 'DESC' : 'ASC') . '&limit=' . $limit);
$data['sort_user_group'] = $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'] . '&filter_title=' . urlencode($filter_title) . '&filter_status=' . $filter_status . '&filter_date_start=' . urlencode($filter_date_start) . '&filter_date_end=' . urlencode($filter_date_end) . '&filter_priority=' . urlencode($filter_priority) . '&filter_due_date_start=' . urlencode($filter_due_date_start) . '&filter_due_date_end=' . urlencode($filter_due_date_end) . '&filter_tag_id=' . urlencode($filter_tag_id) . '&filter_user_group_id=' . urlencode($filter_user_group_id) . '&sort=user_group&order=' . ($sort == 'user_group' && $order == 'ASC' ? 'DESC' : 'ASC') . '&limit=' . $limit);
$data['sort_created_at'] = $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'] . '&filter_title=' . urlencode($filter_title) . '&filter_status=' . $filter_status . '&filter_date_start=' . urlencode($filter_date_start) . '&filter_date_end=' . urlencode($filter_date_end) . '&filter_priority=' . urlencode($filter_priority) . '&filter_due_date_start=' . urlencode($filter_due_date_start) . '&filter_due_date_end=' . urlencode($filter_due_date_end) . '&filter_tag_id=' . urlencode($filter_tag_id) . '&filter_user_group_id=' . urlencode($filter_user_group_id) . '&sort=created_at&order=' . ($sort == 'created_at' && $order == 'ASC' ? 'DESC' : 'ASC') . '&limit=' . $limit);
$data['sort_order'] = $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'] . '&filter_title=' . urlencode($filter_title) . '&filter_status=' . $filter_status . '&filter_date_start=' . urlencode($filter_date_start) . '&filter_date_end=' . urlencode($filter_date_end) . '&filter_priority=' . urlencode($filter_priority) . '&filter_due_date_start=' . urlencode($filter_due_date_start) . '&filter_due_date_end=' . urlencode($filter_due_date_end) . '&filter_tag_id=' . urlencode($filter_tag_id) . '&filter_user_group_id=' . urlencode($filter_user_group_id) . '&sort=sort_order&order=' . ($sort == 'sort_order' && $order == 'ASC' ? 'DESC' : 'ASC') . '&limit=' . $limit);
$data['user_groups'] = $this->model_user_user_group->getUserGroups();
$data['tags'] = $this->model_extension_module_task_manager->getTags();
$data['add'] = $this->url->link('extension/module/task_manager/add', 'user_token=' . $this->session->data['user_token']);
$data['delete'] = $this->url->link('extension/module/task_manager/delete', 'user_token=' . $this->session->data['user_token']);
$data['logs'] = $this->url->link('extension/module/task_manager/logs', 'user_token=' . $this->session->data['user_token']);
$data['calendar'] = $this->url->link('extension/module/task_manager/calendar', 'user_token=' . $this->session->data['user_token']);
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/module/task_manager', $data));
}
public function updateOrder() {
// ... (без изменений)
}
public function logs() {
// ... (без изменений)
}
public function calendar() {
$this->load->language('extension/module/task_manager');
$this->document->setTitle($this->language->get('text_calendar'));
// Подключение FullCalendar
$this->document->addScript('https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js');
$this->document->addStyle('view/stylesheet/task_manager.css');
$data['breadcrumbs'] = [
['text' => $this->language->get('text_home'), 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'])],
['text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'])],
['text' => $this->language->get('text_calendar'), 'href' => $this->url->link('extension/module/task_manager/calendar', 'user_token=' . $this->session->data['user_token'])]
];
$data['events_url'] = $this->url->link('extension/module/task_manager/events', 'user_token=' . $this->session->data['user_token']);
$data['edit_url'] = $this->url->link('extension/module/task_manager/edit', 'user_token=' . $this->session->data['user_token'] . '&task_id=');
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/module/task_manager_calendar', $data));
}
public function events() {
$this->load->model('extension/module/task_manager');
$start_date = isset($this->request->get['start']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['start']) ? $this->request->get['start'] : null;
$end_date = isset($this->request->get['end']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $this->request->get['end']) ? $this->request->get['end'] : null;
$tasks = $this->model_extension_module_task_manager->getCalendarEvents($start_date, $end_date);
$events = array_map(function ($task) {
$color = $task['priority'] === 'high' ? '#dc3545' : ($task['priority'] === 'medium' ? '#ffc107' : '#28a745');
return [
'id' => $task['task_id'],
'title' => $task['title'] . ' (' . $task['status'] . ')',
'start' => $task['due_date'],
'color' => $color,
'url' => $this->url->link('extension/module/task_manager/edit', 'user_token=' . $this->session->data['user_token'] . '&task_id=' . $task['task_id'])
];
}, $tasks);
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($events));
}
public function add() {
$this->logAction("Added task", json_encode($this->request->post));
}
public function edit() {
$this->logAction("Edited task", "Task ID: " . $this->request->get['task_id'] . ", Data: " . json_encode($this->request->post));
}
public function delete() {
$this->logAction("Deleted tasks", json_encode($this->request->post['selected']));
}
}
---
### 3. Создание шаблона для календаря (`admin/view/template/extension/module/task_manager_calendar.twig`)
Создадим файл `admin/view/template/extension/module/task_manager_calendar.twig`:
twig
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<h1>{{ text_calendar }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-calendar"></i> {{ text_calendar }}</h3>
</div>
<div class="panel-body">
<div id="calendar"></div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
events: '{{ events_url }}',
eventClick: function(info) {
window.location.href = info.event.url; // Переход на страницу редактирования
info.jsEvent.preventDefault();
},
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
height: 'auto',
locale: 'ru' // Локализация на русский
});
calendar.render();
});
</script>
{{ footer }}
---
### 4. Обновление языкового файла (`admin/language/ru-ru/extension/module/task_manager.php`)
Добавим перевод для календаря:
php
<?php
// Heading
$_['heading_title'] = 'Менеджер задач';
// Text
$_['text_logs'] = 'Логи';
$_['text_home'] = 'Главная';
$_['text_calendar'] = 'Календарь';
// DateTime Format
$_['datetime_format'] = 'd.m.Y H:i:s';
---
### 5. Обновление бокового меню (`admin/controller/common/column_left.php`)
Добавим пункт "Календарь" в меню (или используйте OCMOD):
php
$data['menus'][] = [
'id' => 'menu-task-manager',
'icon' => 'fa-tasks',
'name' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'], true),
'children' => [
[
'name' => 'Задачи',
'href' => $this->url->link('extension/module/task_manager', 'user_token=' . $this->session->data['user_token'], true),
'children' => []
],
[
'name' => 'Логи',
'href' => $this->url->link('extension/module/task_manager/logs', 'user_token=' . $this->session->data['user_token'], true),
'children' => []
],
[
'name' => 'Календарь',
'href' => $this->url->link('extension/module/task_manager/calendar', 'user_token=' . $this->session->data['user_token'], true),
'children' => []
]
]
];
`
---
### Что сделано:
1. Модель:
- Добавлен метод `getCalendarEvents` для получения задач с датами выполнения в заданном диапазоне.
2. Контроллер:
- Метод `calendar` отображает страницу с FullCalendar.
- Метод `events` возвращает задачи в формате JSON, совместимом с FullCalendar:
- `id`: ID задачи.
- `title`: Название задачи с указанием статуса.
- `start`: Дата выполнения (`due_date`).
- `color`: Цвет события в зависимости от приоритета (красный для `high`, жёлтый для `medium`, зелёный для `low`).
- `url`: Ссылка на страницу редактирования задачи.
3. Шаблон:
- Создан `task_manager_calendar.twig` с подключением FullCalendar через CDN.
- Календарь поддерживает переключение видов (месяц, неделя, день) и кликабельные события.
4. Интеграция в интерфейс:
- Добавлена ссылка на календарь в хлебные крошки на главной странице задач и в боковое меню.
---
### Как это работает:
- Перейдите по адресу `index.php?route=extension/module/task_manager/calendar&user_token=YOUR_TOKEN`.
- Вы увидите календарь, где задачи с датами выполнения отображаются как события.
- Цвет события зависит от приоритета: красный (high), жёлтый (medium), зелёный (low).
- Клик по событию перенаправляет на страницу редактирования задачи.
---
### Пример события в JSON:
[
{
"id": 1,
"title": "Test Task (open)",
"start": "2025-04-15",
"color": "#28a745",
"url": "index.php?route=extension/module/task_manager/edit&user_token=YOUR_TOKEN&task_id=1"
}
]
---
### Дополнительные рекомендации:
1. Drag-and-Drop в календаре:
- FullCalendar поддерживает перетаскивание событий. Чтобы обновлять due_date при перетаскивании, добавьте обработчик eventDrop:
eventDrop: function(info) {
fetch('index.php?route=extension/module/task_manager/edit&user_token={{ user_token }}', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'task_id=' + info.event.id + '&due_date=' + info.event.startStr
}).then(response => response.json()).then(data => {
if (data.success) alert('Дата обновлена');
});
}
- Добавьте соответствующий код в метод edit для обработки POST-запроса.
2. Фильтры:
- Добавьте фильтры (статус, приоритет) на страницу календаря, передавая их в events_url.
3. Локализация:
- FullCalendar уже локализован на русский (locale: 'ru'), но можно настроить формат дат через dateClick или eventTimeFormat.
Теперь задачи интегрированы с календарем! Если нужны доработки (например, перетаскивание событий или фильтры), дайте знать!