Обзор проекта
To-Do List Manager — это современное веб-приложение для управления задачами, разработанное с использованием чистого PHP и архитектурных паттернов MVC (Model-View-Controller) и ООП (Объектно-Ориентированное Программирование). Приложение позволяет пользователям эффективно организовывать свои задачи, устанавливать приоритеты, отслеживать прогресс и сотрудничать с метками.
Ключевые особенности
Основной функционал
- Полная аутентификация пользователей (регистрация, вход, выход)
- CRUD операции для задач (Создание, Чтение, Обновление, Удаление)
- Категоризация задач по статусам (ожидание, в работе, завершено)
- Приоритизация задач (низкий, средний, высокий)
- Система меток/тегов с цветовым кодированием
- Установка сроков выполнения с отслеживанием просроченных задач
- Поиск и фильтрация задач по различным параметрам
Продвинутые возможности
- AJAX-обновление статуса без перезагрузки страницы
- Интерактивный поиск с автодополнением
- Динамическая статистика выполнения задач
- Адаптивный дизайн на Bootstrap 5
- Валидация форм на стороне сервера и клиента
- Сессии и безопасность с хешированием паролей
Архитектура проекта
Структура MVC
Код:
📁 app/
├── 📁 Controllers/ # Контроллеры (бизнес-логика)
├── 📁 Models/ # Модели (работа с данными)
├── 📁 Views/ # Представления (HTML шаблоны)
└── 📁 Core/ # Ядро системы
Модели данных
- User — управление пользователями
- Регистрация и аутентификация
- Связь с задачами
- Task — основная модель задач
- Все CRUD операции
- Статусы и приоритеты
- Методы для работы с метками
- Tag — система меток
- Цветовое кодирование
- Связь многие-ко-многим с задачами
Технологический стек
Backend
- PHP 7.4+ с ООП подходами
- MySQL 5.7+ с отношениями между таблицами
- PDO для безопасной работы с БД
- Сессии PHP для управления состояниями
Frontend
- Bootstrap 5 для адаптивного дизайна
- jQuery для AJAX запросов
- JavaScript ES6 для интерактивности
- CSS3 с кастомными стилями
Инструменты
- Composer для автозагрузки классов
- Git для контроля версий
- Apache/Nginx для веб-сервера
Основные модули
1. Модуль аутентификации
- Регистрация с проверкой уникальности
- Вход с запоминанием сессии
- Защита паролей с password_hash()
2. Модуль задач
- Создание задач с описанием и сроками
- Фильтрация по статусу/приоритету/меткам
- Быстрое изменение статуса через AJAX
- Визуализация просроченных задач
3. Модуль меток
- Создание цветных меток
- Привязка к задачам
- Фильтрация по меткам
4. Модуль статистики
- Отслеживание прогресса
- Количество задач по статусам
- Отслеживание просроченных задач
Цели проекта
Учебные цели
- Показать практическое применение MVC в PHP
- Демонстрация ООП принципов на реальном проекте
- Интеграция AJAX в традиционное PHP приложение
- Создание полноценной CRUD системы с аутентификацией
Практические цели
- Создание готового к использованию приложения
- База для портфолио PHP разработчика
- Учебное пособие для начинающих PHP разработчиков
- Шаблон для собственных проектов
Установка и развертывание
Требования
- Веб-сервер (Apache/Nginx)
- PHP 7.4+
- MySQL 5.7+
- Composer
Интерфейс пользователя
Десктоп версия
- Панель навигации с основными разделами
- Боковая панель с фильтрами и статистикой
- Основная область с таблицей задач
- Модальные окна для форм
Мобильная версия
- Адаптивный дизайн Bootstrap 5
- Упрощенная навигация
- Оптимизированные формы
Безопасность
Реализованные меры
- Хеширование паролей с password_hash()
- Подготовленные SQL запросы (PDO)
- Валидация входных данных
- Защита сессий
- CSRF защита (в расширенной версии)
Производительность
Оптимизации
- Кэширование статических файлов
- Индексирование БД
- AJAX для частичных обновлений
- Минимизация запросов к БД
Возможности для расширения
Планируемые улучшения
- API для мобильных приложений
- Уведомления по email
- Экспорт в PDF/Excel
- Календарный вид задач
- Совместный доступ к задачам
- Вложения файлов
- Комментарии к задачам
- Повторяющиеся задачи
- Интеграция с Google Calendar
- Темная тема
Образовательная ценность
Для начинающих разработчиков
- Понимание MVC на практике
- Работа с БД через PDO
- Сессии и аутентификация
- AJAX в PHP приложениях
- ООП в веб-разработке
Для опытных разработчиков
- Архитектурные решения
- Паттерны проектирования
- Оптимизация кода
- Безопасность веб-приложений
Интеграционные возможности
Внешние сервисы
- Google Calendar API для синхронизации
- Email сервисы для уведомлений
- Мобильные приложения через REST API
- Виджеты для Dashboard
Уникальные особенности
1. Гибкая система меток
- Цветовое кодирование
- Группировка задач
- Быстрая фильтрация
2. Интерактивный интерфейс
- Drag & Drop (в планах)
- Быстрые действия
- Визуальные статусы
3. Модульность архитектуры
- Легко расширяемый
- Переиспользуемый код
- Четкое разделение ответственности
Документация
Включенная документация
- Комментарии в коде
- Структура БД
- API endpoints (в разработке)
- Руководство по установке
Технологический стек
- PHP 7.4+
- MySQL 5.7+
- Bootstrap 5 (для фронтенда)
- jQuery (для AJAX)
- Composer (для автозагрузки классов)
1. Архитектура проекта
Создадим следующую структуру проекта:
Код:
todo-app/
├── app/
│ ├── Controllers/
│ ├── Models/
│ ├── Views/
│ └── Core/
├── public/
│ ├── css/
│ ├── js/
│ └── index.php
├── config/
├── vendor/
├── .htaccess
├── composer.json
└── README.md
2. Настройка базы данных
Создание таблиц
Код:
-- config/database.sql
CREATE DATABASE IF NOT EXISTS todo_app;
USE todo_app;
-- Таблица пользователей
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Таблица задач
CREATE TABLE tasks (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending',
priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
due_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_status (status),
INDEX idx_priority (priority),
INDEX idx_due_date (due_date)
);
-- Таблица меток (тегов) для задач
CREATE TABLE tags (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7) DEFAULT '#007bff'
);
-- Связующая таблика задач и меток
CREATE TABLE task_tags (
task_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (task_id, tag_id),
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
-- Вставка тестовых данных
INSERT INTO tags (name, color) VALUES
('Работа', '#dc3545'),
('Личное', '#28a745'),
('Срочно', '#ffc107'),
('Идеи', '#17a2b8');
-- Тестовый пользователь (пароль: password123)
INSERT INTO users (username, email, password_hash) VALUES
('demo', 'demo@example.com', '$2y$10$YourHashedPasswordHere');
Конфигурация подключения
PHP:
<?php
// config/database.php
return [
'host' => 'localhost',
'database' => 'todo_app',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
];
3. Создание моделей
Базовый класс модели
PHP:
<?php
// app/Core/Model.php
namespace App\Core;
use PDO;
use PDOException;
abstract class Model
{
protected static $connection = null;
protected $table;
protected $primaryKey = 'id';
protected $fillable = [];
protected $attributes = [];
public function __construct(array $attributes = [])
{
$this->fill($attributes);
}
protected static function getConnection()
{
if (self::$connection === null) {
$config = require __DIR__ . '/../../config/database.php';
try {
$dsn = "mysql:host={$config['host']};dbname={$config['database']};charset={$config['charset']}";
self::$connection = new PDO($dsn, $config['username'], $config['password'], $config['options']);
} catch (PDOException $e) {
die('Database connection failed: ' . $e->getMessage());
}
}
return self::$connection;
}
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if (in_array($key, $this->fillable)) {
$this->attributes[$key] = $value;
}
}
return $this;
}
public function __get($name)
{
return $this->attributes[$name] ?? null;
}
public function __set($name, $value)
{
if (in_array($name, $this->fillable)) {
$this->attributes[$name] = $value;
}
}
public function save()
{
if (empty($this->attributes[$this->primaryKey])) {
return $this->insert();
}
return $this->update();
}
protected function insert()
{
$columns = implode(', ', array_keys($this->attributes));
$placeholders = ':' . implode(', :', array_keys($this->attributes));
$sql = "INSERT INTO {$this->table} ($columns) VALUES ($placeholders)";
$stmt = self::getConnection()->prepare($sql);
foreach ($this->attributes as $key => $value) {
$stmt->bindValue(":$key", $value);
}
if ($stmt->execute()) {
$this->attributes[$this->primaryKey] = self::getConnection()->lastInsertId();
return true;
}
return false;
}
protected function update()
{
$setClause = [];
foreach ($this->attributes as $key => $value) {
if ($key !== $this->primaryKey) {
$setClause[] = "$key = :$key";
}
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $setClause) .
" WHERE {$this->primaryKey} = :id";
$stmt = self::getConnection()->prepare($sql);
foreach ($this->attributes as $key => $value) {
$stmt->bindValue(":$key", $value);
}
$stmt->bindValue(':id', $this->attributes[$this->primaryKey]);
return $stmt->execute();
}
public static function find($id)
{
$instance = new static();
$sql = "SELECT * FROM {$instance->table} WHERE {$instance->primaryKey} = :id LIMIT 1";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch();
if ($data) {
return new static($data);
}
return null;
}
public static function all()
{
$instance = new static();
$sql = "SELECT * FROM {$instance->table}";
$stmt = self::getConnection()->query($sql);
$results = [];
while ($row = $stmt->fetch()) {
$results[] = new static($row);
}
return $results;
}
public function delete()
{
if (empty($this->attributes[$this->primaryKey])) {
return false;
}
$sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = :id";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':id', $this->attributes[$this->primaryKey], PDO::PARAM_INT);
return $stmt->execute();
}
public static function where($column, $operator, $value = null)
{
if (func_num_args() === 2) {
$value = $operator;
$operator = '=';
}
$instance = new static();
$queryBuilder = new QueryBuilder($instance->table);
return $queryBuilder->where($column, $operator, $value);
}
public function toArray()
{
return $this->attributes;
}
}
class QueryBuilder
{
private $table;
private $conditions = [];
private $params = [];
private $orderBy = '';
private $limit = '';
public function __construct($table)
{
$this->table = $table;
}
public function where($column, $operator, $value)
{
$this->conditions[] = "$column $operator :{$column}";
$this->params[$column] = $value;
return $this;
}
public function orderBy($column, $direction = 'ASC')
{
$this->orderBy = " ORDER BY $column $direction";
return $this;
}
public function limit($limit, $offset = 0)
{
$this->limit = " LIMIT $offset, $limit";
return $this;
}
public function get()
{
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->conditions)) {
$sql .= " WHERE " . implode(' AND ', $this->conditions);
}
$sql .= $this->orderBy . $this->limit;
$stmt = Model::getConnection()->prepare($sql);
foreach ($this->params as $key => $value) {
$stmt->bindValue(":$key", $value);
}
$stmt->execute();
$results = [];
$class = $this->getModelClass();
while ($row = $stmt->fetch()) {
$results[] = new $class($row);
}
return $results;
}
private function getModelClass()
{
// Определяем класс модели по имени таблицы
$modelName = str_replace(' ', '', ucwords(str_replace('_', ' ', $this->table)));
return "App\\Models\\{$modelName}";
}
}
Модель задачи
PHP:
<?php
// app/Models/Task.php
namespace App\Models;
use App\Core\Model;
class Task extends Model
{
protected $table = 'tasks';
protected $primaryKey = 'id';
protected $fillable = [
'user_id', 'title', 'description', 'status',
'priority', 'due_date'
];
public function user()
{
return User::find($this->user_id);
}
public function tags()
{
$sql = "SELECT t.* FROM tags t
JOIN task_tags tt ON t.id = tt.tag_id
WHERE tt.task_id = :task_id";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':task_id', $this->id, \PDO::PARAM_INT);
$stmt->execute();
$tags = [];
while ($row = $stmt->fetch()) {
$tags[] = new Tag($row);
}
return $tags;
}
public function attachTag($tagId)
{
$sql = "INSERT IGNORE INTO task_tags (task_id, tag_id) VALUES (:task_id, :tag_id)";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':task_id', $this->id, \PDO::PARAM_INT);
$stmt->bindValue(':tag_id', $tagId, \PDO::PARAM_INT);
return $stmt->execute();
}
public function detachTag($tagId)
{
$sql = "DELETE FROM task_tags WHERE task_id = :task_id AND tag_id = :tag_id";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':task_id', $this->id, \PDO::PARAM_INT);
$stmt->bindValue(':tag_id', $tagId, \PDO::PARAM_INT);
return $stmt->execute();
}
public function getStatusBadge()
{
$badges = [
'pending' => '<span class="badge bg-secondary">В ожидании</span>',
'in_progress' => '<span class="badge bg-warning">В работе</span>',
'completed' => '<span class="badge bg-success">Завершено</span>'
];
return $badges[$this->status] ?? '';
}
public function getPriorityBadge()
{
$badges = [
'low' => '<span class="badge bg-info">Низкий</span>',
'medium' => '<span class="badge bg-primary">Средний</span>',
'high' => '<span class="badge bg-danger">Высокий</span>'
];
return $badges[$this->priority] ?? '';
}
public function isOverdue()
{
if (!$this->due_date) {
return false;
}
$dueDate = new \DateTime($this->due_date);
$today = new \DateTime('today');
return $dueDate < $today && $this->status !== 'completed';
}
public static function getStatistics($userId)
{
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN due_date < CURDATE() AND status != 'completed' THEN 1 ELSE 0 END) as overdue
FROM tasks
WHERE user_id = :user_id";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':user_id', $userId, \PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch();
}
}
Модель пользователя
PHP:
<?php
// app/Models/User.php
namespace App\Models;
use App\Core\Model;
class User extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $fillable = ['username', 'email', 'password_hash'];
public function tasks()
{
return Task::where('user_id', '=', $this->id)->get();
}
public static function register($username, $email, $password)
{
$user = new static();
$user->username = $username;
$user->email = $email;
$user->password_hash = password_hash($password, PASSWORD_BCRYPT);
return $user->save();
}
public static function authenticate($username, $password)
{
$sql = "SELECT * FROM users WHERE username = :username OR email = :email LIMIT 1";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':username', $username);
$stmt->bindValue(':email', $username);
$stmt->execute();
$userData = $stmt->fetch();
if ($userData && password_verify($password, $userData['password_hash'])) {
return new static($userData);
}
return null;
}
public function changePassword($newPassword)
{
$this->password_hash = password_hash($newPassword, PASSWORD_BCRYPT);
return $this->save();
}
}
Модель метки
PHP:
<?php
// app/Models/Tag.php
namespace App\Models;
use App\Core\Model;
class Tag extends Model
{
protected $table = 'tags';
protected $primaryKey = 'id';
protected $fillable = ['name', 'color'];
public function tasks()
{
$sql = "SELECT t.* FROM tasks t
JOIN task_tags tt ON t.id = tt.task_id
WHERE tt.tag_id = :tag_id";
$stmt = self::getConnection()->prepare($sql);
$stmt->bindValue(':tag_id', $this->id, \PDO::PARAM_INT);
$stmt->execute();
$tasks = [];
while ($row = $stmt->fetch()) {
$tasks[] = new Task($row);
}
return $tasks;
}
public function getColorStyle()
{
return "style='background-color: {$this->color}; color: white;'";
}
}
4. Реализация контроллеров
Базовый контроллер
PHP:
<?php
// app/Core/Controller.php
namespace App\Core;
class Controller
{
protected function render($view, $data = [])
{
extract($data);
$viewPath = __DIR__ . "/../Views/{$view}.php";
if (!file_exists($viewPath)) {
throw new \Exception("View {$view} not found");
}
require $viewPath;
}
protected function json($data, $statusCode = 200)
{
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
protected function redirect($url)
{
header("Location: $url");
exit;
}
protected function isAjax()
{
return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
}
protected function getCurrentUser()
{
session_start();
return $_SESSION['user'] ?? null;
}
protected function requireAuth()
{
$user = $this->getCurrentUser();
if (!$user) {
$this->redirect('/login');
}
return $user;
}
protected function validate($data, $rules)
{
$errors = [];
foreach ($rules as $field => $rule) {
$value = $data[$field] ?? null;
$ruleParts = explode('|', $rule);
foreach ($ruleParts as $singleRule) {
if ($singleRule === 'required' && empty($value)) {
$errors[$field][] = "Поле обязательно для заполнения";
}
if (strpos($singleRule, 'min:') === 0) {
$min = (int) substr($singleRule, 4);
if (strlen($value) < $min) {
$errors[$field][] = "Минимальная длина: {$min} символов";
}
}
if ($singleRule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "Некорректный email адрес";
}
}
}
return $errors;
}
}
Контроллер задач
PHP:
<?php
// app/Controllers/TaskController.php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Task;
use App\Models\Tag;
class TaskController extends Controller
{
public function index()
{
$user = $this->requireAuth();
$status = $_GET['status'] ?? null;
$priority = $_GET['priority'] ?? null;
$tagId = $_GET['tag_id'] ?? null;
$query = Task::where('user_id', '=', $user->id);
if ($status) {
$query->where('status', '=', $status);
}
if ($priority) {
$query->where('priority', '=', $priority);
}
if ($tagId) {
// Фильтрация по метке через подзапрос
$subquery = "id IN (SELECT task_id FROM task_tags WHERE tag_id = :tag_id)";
// В реальном проекте нужно расширить QueryBuilder для поддержки подзапросов
}
$tasks = $query->orderBy('due_date', 'ASC')->get();
$tags = Tag::all();
$statistics = Task::getStatistics($user->id);
$this->render('tasks/index', [
'tasks' => $tasks,
'tags' => $tags,
'statistics' => $statistics,
'filters' => [
'status' => $status,
'priority' => $priority,
'tag_id' => $tagId
]
]);
}
public function create()
{
$user = $this->requireAuth();
$tags = Tag::all();
$this->render('tasks/create', ['tags' => $tags]);
}
public function store()
{
$user = $this->requireAuth();
$errors = $this->validate($_POST, [
'title' => 'required|min:3',
'description' => 'required|min:10',
'due_date' => 'required'
]);
if (!empty($errors)) {
$tags = Tag::all();
$this->render('tasks/create', [
'errors' => $errors,
'old' => $_POST,
'tags' => $tags
]);
return;
}
$task = new Task();
$task->user_id = $user->id;
$task->title = $_POST['title'];
$task->description = $_POST['description'];
$task->status = $_POST['status'] ?? 'pending';
$task->priority = $_POST['priority'] ?? 'medium';
$task->due_date = $_POST['due_date'];
if ($task->save()) {
// Привязка меток
$tagIds = $_POST['tags'] ?? [];
foreach ($tagIds as $tagId) {
$task->attachTag($tagId);
}
$_SESSION['success'] = 'Задача успешно создана!';
$this->redirect('/tasks');
} else {
$_SESSION['error'] = 'Ошибка при создании задачи';
$this->redirect('/tasks/create');
}
}
public function show($id)
{
$user = $this->requireAuth();
$task = Task::find($id);
if (!$task || $task->user_id != $user->id) {
$_SESSION['error'] = 'Задача не найдена';
$this->redirect('/tasks');
}
$this->render('tasks/show', ['task' => $task]);
}
public function edit($id)
{
$user = $this->requireAuth();
$task = Task::find($id);
$tags = Tag::all();
if (!$task || $task->user_id != $user->id) {
$_SESSION['error'] = 'Задача не найдена';
$this->redirect('/tasks');
}
$this->render('tasks/edit', [
'task' => $task,
'tags' => $tags
]);
}
public function update($id)
{
$user = $this->requireAuth();
$task = Task::find($id);
if (!$task || $task->user_id != $user->id) {
$_SESSION['error'] = 'Задача не найдена';
$this->redirect('/tasks');
}
$errors = $this->validate($_POST, [
'title' => 'required|min:3',
'description' => 'required|min:10'
]);
if (!empty($errors)) {
$tags = Tag::all();
$this->render('tasks/edit', [
'task' => $task,
'errors' => $errors,
'tags' => $tags
]);
return;
}
$task->title = $_POST['title'];
$task->description = $_POST['description'];
$task->status = $_POST['status'];
$task->priority = $_POST['priority'];
$task->due_date = $_POST['due_date'];
if ($task->save()) {
// Обновление меток
$currentTagIds = array_map(function($tag) {
return $tag->id;
}, $task->tags());
$newTagIds = $_POST['tags'] ?? [];
// Удалить отсутствующие
foreach ($currentTagIds as $tagId) {
if (!in_array($tagId, $newTagIds)) {
$task->detachTag($tagId);
}
}
// Добавить новые
foreach ($newTagIds as $tagId) {
if (!in_array($tagId, $currentTagIds)) {
$task->attachTag($tagId);
}
}
$_SESSION['success'] = 'Задача успешно обновлена!';
$this->redirect('/tasks');
} else {
$_SESSION['error'] = 'Ошибка при обновлении задачи';
$this->redirect("/tasks/{$id}/edit");
}
}
public function destroy($id)
{
$user = $this->requireAuth();
$task = Task::find($id);
if (!$task || $task->user_id != $user->id) {
$_SESSION['error'] = 'Задача не найдена';
$this->redirect('/tasks');
}
if ($task->delete()) {
$_SESSION['success'] = 'Задача успешно удалена!';
} else {
$_SESSION['error'] = 'Ошибка при удалении задачи';
}
$this->redirect('/tasks');
}
public function updateStatus()
{
if (!$this->isAjax()) {
$this->json(['error' => 'Invalid request'], 400);
}
$user = $this->getCurrentUser();
if (!$user) {
$this->json(['error' => 'Unauthorized'], 401);
}
$taskId = $_POST['task_id'] ?? null;
$status = $_POST['status'] ?? null;
if (!$taskId || !$status) {
$this->json(['error' => 'Missing parameters'], 400);
}
$task = Task::find($taskId);
if (!$task || $task->user_id != $user->id) {
$this->json(['error' => 'Task not found'], 404);
}
$validStatuses = ['pending', 'in_progress', 'completed'];
if (!in_array($status, $validStatuses)) {
$this->json(['error' => 'Invalid status'], 400);
}
$task->status = $status;
if ($task->save()) {
$this->json([
'success' => true,
'status' => $status,
'badge' => $task->getStatusBadge()
]);
} else {
$this->json(['error' => 'Update failed'], 500);
}
}
public function search()
{
if (!$this->isAjax()) {
$this->json(['error' => 'Invalid request'], 400);
}
$user = $this->getCurrentUser();
if (!$user) {
$this->json(['error' => 'Unauthorized'], 401);
}
$query = $_GET['q'] ?? '';
if (strlen($query) < 2) {
$this->json(['tasks' => []]);
}
$sql = "SELECT * FROM tasks
WHERE user_id = :user_id
AND (title LIKE :query OR description LIKE :query)
ORDER BY created_at DESC
LIMIT 10";
$stmt = \App\Core\Model::getConnection()->prepare($sql);
$stmt->bindValue(':user_id', $user->id, \PDO::PARAM_INT);
$stmt->bindValue(':query', "%{$query}%");
$stmt->execute();
$tasks = [];
while ($row = $stmt->fetch()) {
$task = new Task($row);
$tasks[] = [
'id' => $task->id,
'title' => $task->title,
'status' => $task->status,
'priority' => $task->priority,
'due_date' => $task->due_date,
'status_badge' => $task->getStatusBadge(),
'priority_badge' => $task->getPriorityBadge(),
'is_overdue' => $task->isOverdue()
];
}
$this->json(['tasks' => $tasks]);
}
}