С 9:00 до 21:00 Без выходных

+Интеграция списка задач с календарем

Чтобы интегрировать список задач из модуля "Менеджер задач" в 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.


Теперь задачи интегрированы с календарем! Если нужны доработки (например, перетаскивание событий или фильтры), дайте знать!