$value) { if ($value === null || $value === '') { continue; } $filtered[$key] = $value; } return $filtered ? ('?' . http_build_query($filtered)) : ''; } function redirectWithState(array $params): void { header('Location: index.php' . buildQuery($params)); exit; } function setFlash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function getFlash(): ?array { if (!isset($_SESSION['flash'])) { return null; } $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } function ensureTodoTable(PDO $pdo): void { $pdo->exec( "CREATE TABLE IF NOT EXISTS todos ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(140) NOT NULL, description TEXT NULL, priority ENUM('low', 'medium', 'high') NOT NULL DEFAULT 'medium', is_completed TINYINT(1) NOT NULL DEFAULT 0, completed_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_completed (is_completed), INDEX idx_priority (priority), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $count = (int) $pdo->query('SELECT COUNT(*) FROM todos')->fetchColumn(); if ($count === 0) { $seed = $pdo->prepare( 'INSERT INTO todos (title, description, priority, is_completed, completed_at) VALUES (:title, :description, :priority, :is_completed, :completed_at)' ); $demoTasks = [ [ 'title' => 'Подготовить список задач на неделю', 'description' => 'Собрать важные личные и рабочие дела, чтобы всё было в одном месте.', 'priority' => 'high', 'is_completed' => 0, 'completed_at' => null, ], [ 'title' => 'Разобрать входящие письма', 'description' => 'Оставить только действительно нужные письма и отметить follow-up.', 'priority' => 'medium', 'is_completed' => 0, 'completed_at' => null, ], [ 'title' => 'Забронировать тренировку', 'description' => 'Выбрать время на этой неделе и подтвердить занятие.', 'priority' => 'low', 'is_completed' => 1, 'completed_at' => date('Y-m-d H:i:s', strtotime('-1 day')), ], ]; foreach ($demoTasks as $task) { $seed->execute([ ':title' => $task['title'], ':description' => $task['description'], ':priority' => $task['priority'], ':is_completed' => $task['is_completed'], ':completed_at' => $task['completed_at'], ]); } } } $pdo = db(); ensureTodoTable($pdo); if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } $csrfToken = $_SESSION['csrf_token']; $status = $_GET['status'] ?? 'all'; $status = in_array($status, ['all', 'active', 'completed'], true) ? $status : 'all'; $sort = $_GET['sort'] ?? 'newest'; $sort = in_array($sort, ['newest', 'oldest', 'priority'], true) ? $sort : 'newest'; $q = trim((string) ($_GET['q'] ?? '')); $selectedTaskId = filter_input(INPUT_GET, 'task', FILTER_VALIDATE_INT) ?: null; $editTaskId = filter_input(INPUT_GET, 'edit', FILTER_VALIDATE_INT) ?: null; $baseState = [ 'status' => $status, 'sort' => $sort, 'q' => $q, ]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $postedToken = $_POST['csrf_token'] ?? ''; $postStatus = $_POST['status'] ?? 'all'; $postStatus = in_array($postStatus, ['all', 'active', 'completed'], true) ? $postStatus : 'all'; $postSort = $_POST['sort'] ?? 'newest'; $postSort = in_array($postSort, ['newest', 'oldest', 'priority'], true) ? $postSort : 'newest'; $postQ = trim((string) ($_POST['q'] ?? '')); $returnTask = filter_input(INPUT_POST, 'task', FILTER_VALIDATE_INT) ?: null; $redirectState = ['status' => $postStatus, 'sort' => $postSort, 'q' => $postQ]; if ($returnTask) { $redirectState['task'] = $returnTask; } if (!hash_equals($csrfToken, $postedToken)) { setFlash('danger', 'Сессия устарела. Попробуйте ещё раз.'); redirectWithState($redirectState); } $action = $_POST['action'] ?? ''; try { if ($action === 'create') { $title = trim((string) ($_POST['title'] ?? '')); $description = trim((string) ($_POST['description'] ?? '')); $priority = $_POST['priority'] ?? 'medium'; $priority = in_array($priority, ['low', 'medium', 'high'], true) ? $priority : 'medium'; if ($title === '' || utf8Length($title) > 140) { throw new RuntimeException('Название задачи обязательно и должно быть короче 140 символов.'); } if (utf8Length($description) > 1000) { throw new RuntimeException('Описание должно быть короче 1000 символов.'); } $stmt = $pdo->prepare('INSERT INTO todos (title, description, priority) VALUES (:title, :description, :priority)'); $stmt->bindValue(':title', $title); $stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':priority', $priority); $stmt->execute(); $newId = (int) $pdo->lastInsertId(); setFlash('success', 'Задача добавлена.'); $redirectState['task'] = $newId; redirectWithState($redirectState); } if ($action === 'update') { $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT); $title = trim((string) ($_POST['title'] ?? '')); $description = trim((string) ($_POST['description'] ?? '')); $priority = $_POST['priority'] ?? 'medium'; $priority = in_array($priority, ['low', 'medium', 'high'], true) ? $priority : 'medium'; if (!$taskId) { throw new RuntimeException('Не удалось определить задачу для редактирования.'); } if ($title === '' || utf8Length($title) > 140) { throw new RuntimeException('Название задачи обязательно и должно быть короче 140 символов.'); } if (utf8Length($description) > 1000) { throw new RuntimeException('Описание должно быть короче 1000 символов.'); } $stmt = $pdo->prepare('UPDATE todos SET title = :title, description = :description, priority = :priority WHERE id = :id LIMIT 1'); $stmt->bindValue(':title', $title); $stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':priority', $priority); $stmt->bindValue(':id', $taskId, PDO::PARAM_INT); $stmt->execute(); setFlash('success', 'Изменения сохранены.'); $redirectState['task'] = $taskId; redirectWithState($redirectState); } if ($action === 'toggle') { $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT); if (!$taskId) { throw new RuntimeException('Не удалось определить задачу.'); } $stmt = $pdo->prepare('SELECT is_completed FROM todos WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $taskId]); $task = $stmt->fetch(); if (!$task) { throw new RuntimeException('Задача не найдена.'); } $nextState = (int) !$task['is_completed']; $update = $pdo->prepare('UPDATE todos SET is_completed = :is_completed, completed_at = :completed_at WHERE id = :id LIMIT 1'); $update->bindValue(':is_completed', $nextState, PDO::PARAM_INT); $update->bindValue(':completed_at', $nextState ? date('Y-m-d H:i:s') : null, $nextState ? PDO::PARAM_STR : PDO::PARAM_NULL); $update->bindValue(':id', $taskId, PDO::PARAM_INT); $update->execute(); setFlash('success', $nextState ? 'Задача отмечена как выполненная.' : 'Задача снова активна.'); $redirectState['task'] = $taskId; redirectWithState($redirectState); } if ($action === 'delete') { $taskId = filter_input(INPUT_POST, 'task_id', FILTER_VALIDATE_INT); if (!$taskId) { throw new RuntimeException('Не удалось определить задачу.'); } $stmt = $pdo->prepare('DELETE FROM todos WHERE id = :id LIMIT 1'); $stmt->bindValue(':id', $taskId, PDO::PARAM_INT); $stmt->execute(); setFlash('success', 'Задача удалена.'); unset($redirectState['task']); redirectWithState($redirectState); } setFlash('warning', 'Неизвестное действие.'); redirectWithState($redirectState); } catch (Throwable $exception) { setFlash('danger', $exception->getMessage()); redirectWithState($redirectState); } } $where = []; $params = []; if ($status === 'active') { $where[] = 'is_completed = 0'; } elseif ($status === 'completed') { $where[] = 'is_completed = 1'; } if ($q !== '') { $where[] = '(title LIKE :search OR description LIKE :search)'; $params[':search'] = '%' . $q . '%'; } $whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; $orderSql = match ($sort) { 'oldest' => 'ORDER BY created_at ASC, id ASC', 'priority' => "ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END ASC, is_completed ASC, created_at DESC", default => 'ORDER BY created_at DESC, id DESC', }; $listStmt = $pdo->prepare("SELECT id, title, description, priority, is_completed, completed_at, created_at, updated_at FROM todos {$whereSql} {$orderSql}"); foreach ($params as $key => $value) { $listStmt->bindValue($key, $value); } $listStmt->execute(); $tasks = $listStmt->fetchAll(); $stats = $pdo->query( "SELECT COUNT(*) AS total_count, SUM(CASE WHEN is_completed = 0 THEN 1 ELSE 0 END) AS active_count, SUM(CASE WHEN is_completed = 1 THEN 1 ELSE 0 END) AS completed_count, SUM(CASE WHEN priority = 'high' AND is_completed = 0 THEN 1 ELSE 0 END) AS urgent_count FROM todos" )->fetch() ?: ['total_count' => 0, 'active_count' => 0, 'completed_count' => 0, 'urgent_count' => 0]; $selectedTask = null; if ($selectedTaskId) { $detailStmt = $pdo->prepare('SELECT * FROM todos WHERE id = :id LIMIT 1'); $detailStmt->execute([':id' => $selectedTaskId]); $selectedTask = $detailStmt->fetch() ?: null; } $editingTask = null; if ($editTaskId) { if ($selectedTask && (int) $selectedTask['id'] === $editTaskId) { $editingTask = $selectedTask; } else { $editStmt = $pdo->prepare('SELECT * FROM todos WHERE id = :id LIMIT 1'); $editStmt->execute([':id' => $editTaskId]); $editingTask = $editStmt->fetch() ?: null; } } if (!$selectedTask && !empty($tasks)) { $selectedTask = $tasks[0]; $selectedTaskId = (int) $selectedTask['id']; } $flash = getFlash(); $projectName = trim((string) ($_SERVER['PROJECT_NAME'] ?? '')) ?: 'Task Ledger'; $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Personal todo application with database persistence and a modern focused interface.'; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $pageTitle = $projectName . ' — Personal Todo'; $assetVersion = (string) @filemtime(__DIR__ . '/assets/css/custom.css'); ?> <?= h($pageTitle) ?>
Всего задач Личная база задач
Активные В работе сейчас
Выполнены История прогресса
Высокий приоритет Требуют внимания
Короткое и понятное название задачи.
Управление

Список задач

'Все', 'active' => 'Активные', 'completed' => 'Выполненные'] as $filterValue => $filterLabel): ?> >
Сбросить

Ничего не найдено

Измените фильтры или добавьте первую задачу, чтобы начать список.

Создать задачу
$task['id']])); $editLink = buildQuery(array_merge($baseState, ['task' => $task['id'], 'edit' => $task['id']])); ?>
Детали

Карточка задачи

'Высокий приоритет', 'low' => 'Низкий приоритет', default => 'Средний приоритет', }) ?>

Создана
Последнее обновление
Завершение
Редактировать

Выберите задачу

После выбора здесь появятся детали и быстрые действия.