📋 Обзор проекта​

To-Do List Manager — это современное веб-приложение для управления задачами, разработанное с использованием чистого PHP и архитектурных паттернов MVC (Model-View-Controller) и ООП (Объектно-Ориентированное Программирование). Приложение позволяет пользователям эффективно организовывать свои задачи, устанавливать приоритеты, отслеживать прогресс и сотрудничать с метками.

🎨 Ключевые особенности​

✅ Основной функционал​

  • Полная аутентификация пользователей (регистрация, вход, выход)
  • CRUD операции для задач (Создание, Чтение, Обновление, Удаление)
  • Категоризация задач по статусам (ожидание, в работе, завершено)
  • Приоритизация задач (низкий, средний, высокий)
  • Система меток/тегов с цветовым кодированием
  • Установка сроков выполнения с отслеживанием просроченных задач
  • Поиск и фильтрация задач по различным параметрам

🚀 Продвинутые возможности​

  • AJAX-обновление статуса без перезагрузки страницы
  • Интерактивный поиск с автодополнением
  • Динамическая статистика выполнения задач
  • Адаптивный дизайн на Bootstrap 5
  • Валидация форм на стороне сервера и клиента
  • Сессии и безопасность с хешированием паролей

🏗️ Архитектура проекта​

Структура MVC​

Код:
📁 app/
├── 📁 Controllers/     # Контроллеры (бизнес-логика)
├── 📁 Models/         # Модели (работа с данными)
├── 📁 Views/          # Представления (HTML шаблоны)
└── 📁 Core/           # Ядро системы

Модели данных​

  1. User — управление пользователями
    • Регистрация и аутентификация
    • Связь с задачами
  2. Task — основная модель задач
    • Все CRUD операции
    • Статусы и приоритеты
    • Методы для работы с метками
  3. 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. Модуль статистики​

  • Отслеживание прогресса
  • Количество задач по статусам
  • Отслеживание просроченных задач

🎯 Цели проекта​

Учебные цели​

  1. Показать практическое применение MVC в PHP
  2. Демонстрация ООП принципов на реальном проекте
  3. Интеграция AJAX в традиционное PHP приложение
  4. Создание полноценной CRUD системы с аутентификацией

Практические цели​

  1. Создание готового к использованию приложения
  2. База для портфолио PHP разработчика
  3. Учебное пособие для начинающих PHP разработчиков
  4. Шаблон для собственных проектов

🔧 Установка и развертывание​

Требования​

  • Веб-сервер (Apache/Nginx)
  • PHP 7.4+
  • MySQL 5.7+
  • Composer

📱 Интерфейс пользователя​

Десктоп версия​

  • Панель навигации с основными разделами
  • Боковая панель с фильтрами и статистикой
  • Основная область с таблицей задач
  • Модальные окна для форм

Мобильная версия​

  • Адаптивный дизайн Bootstrap 5
  • Упрощенная навигация
  • Оптимизированные формы

🛡️ Безопасность​

Реализованные меры​

  • Хеширование паролей с password_hash()
  • Подготовленные SQL запросы (PDO)
  • Валидация входных данных
  • Защита сессий
  • CSRF защита (в расширенной версии)

📈 Производительность​

Оптимизации​

  • Кэширование статических файлов
  • Индексирование БД
  • AJAX для частичных обновлений
  • Минимизация запросов к БД

🚀 Возможности для расширения​

Планируемые улучшения​

  1. API для мобильных приложений
  2. Уведомления по email
  3. Экспорт в PDF/Excel
  4. Календарный вид задач
  5. Совместный доступ к задачам
  6. Вложения файлов
  7. Комментарии к задачам
  8. Повторяющиеся задачи
  9. Интеграция с Google Calendar
  10. Темная тема

🎓 Образовательная ценность​

Для начинающих разработчиков​

  • Понимание 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]);
    }
}