Мини-система обработки обращений со статусной моделью


Цель

Разработать систему тикетов с возможностью:

  • создавать обращения
  • менять статус
  • удалять записи
  • фильтровать по статусу
  • расширять статусную модель через отдельный подключаемый модуль

Структура проекта

tickets/
  index.html
  app.js
  api.php
  config.php
  status_plugin.php
  data/
    tickets.sqlite

1. Базовая версия системы


Файл config.php

<?php

function get_statuses() {
    return [
        'Новое',
        'В работе',
        'Завершено'
    ];
}

Файл api.php

<?php
header('Content-Type: application/json');

require_once 'config.php';

$db = new PDO('sqlite:' . __DIR__ . '/data/tickets.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec("
CREATE TABLE IF NOT EXISTS tickets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    email TEXT,
    message TEXT,
    status TEXT,
    created_at TEXT
)
");

$method = $_SERVER['REQUEST_METHOD'];

if ($method === 'POST') {
    $data = json_decode(file_get_contents('php://input'), true);

    $stmt = $db->prepare("
        INSERT INTO tickets (name, email, message, status, created_at)
        VALUES (:name, :email, :message, :status, :created_at)
    ");

    $stmt->execute([
        ':name' => $data['name'],
        ':email' => $data['email'],
        ':message' => $data['message'],
        ':status' => 'Новое',
        ':created_at' => date('Y-m-d H:i:s')
    ]);

    echo json_encode(['status' => 'created']);
    exit;
}

if ($method === 'PUT') {
    $data = json_decode(file_get_contents('php://input'), true);

    $statuses = get_statuses();

    if (!in_array($data['status'], $statuses)) {
        echo json_encode(['error' => 'Invalid status']);
        exit;
    }

    $stmt = $db->prepare("
        UPDATE tickets
        SET status = :status
        WHERE id = :id
    ");

    $stmt->execute([
        ':status' => $data['status'],
        ':id' => $data['id']
    ]);

    echo json_encode(['status' => 'updated']);
    exit;
}

if ($method === 'DELETE') {
    parse_str($_SERVER['QUERY_STRING'], $query);

    $stmt = $db->prepare("DELETE FROM tickets WHERE id = :id");
    $stmt->execute([':id' => $query['id']]);

    echo json_encode(['status' => 'deleted']);
    exit;
}

$status_filter = $_GET['status'] ?? null;

if ($status_filter) {
    $stmt = $db->prepare("
        SELECT * FROM tickets
        WHERE status = :status
        ORDER BY id DESC
    ");
    $stmt->execute([':status' => $status_filter]);
} else {
    $stmt = $db->query("
        SELECT * FROM tickets
        ORDER BY id DESC
    ");
}

echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));

Файл index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Ticket System</title>
</head>
<body>

<h2>Создать обращение</h2>

<input id="name" placeholder="Имя">
<input id="email" placeholder="Email">
<textarea id="message" placeholder="Сообщение"></textarea>
<button onclick="createTicket()">Создать</button>

<h2>Фильтр</h2>
<select id="filter" onchange="load()">
  <option value="">Все</option>
  <option>Новое</option>
  <option>В работе</option>
  <option>Завершено</option>
</select>

<h2>Список</h2>
<div id="list"></div>

<script src="app.js"></script>
</body>
</html>

Файл app.js

async function createTicket() {
    const data = {
        name: name.value,
        email: email.value,
        message: message.value
    };

    await fetch('api.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(data)
    });

    load();
}

async function changeStatus(id, status) {
    await fetch('api.php', {
        method: 'PUT',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({id, status})
    });

    load();
}

async function deleteTicket(id) {
    await fetch('api.php?id=' + id, {
        method: 'DELETE'
    });

    load();
}

async function load() {
    const status = filter.value;
    const url = status ? `api.php?status=${status}` : 'api.php';

    const res = await fetch(url);
    const data = await res.json();

    list.innerHTML = '';

    data.forEach(item => {
        const div = document.createElement('div');
        div.innerHTML = `
            <b>${item.name}</b> (${item.status})
            <br>${item.message}
            <br>
            <button onclick="changeStatus(${item.id}, 'В работе')">В работу</button>
            <button onclick="changeStatus(${item.id}, 'Завершено')">Завершить</button>
            <button onclick="deleteTicket(${item.id})">Удалить</button>
            <hr>
        `;
        list.appendChild(div);
    });
}

load();

2. Задание на изменение архитектуры

Добавить подключаемый модуль статусной модели

Создать файл status_plugin.php, который расширяет список статусов.


Новый файл status_plugin.php

<?php

function extend_statuses($statuses) {
    $statuses[] = 'Отклонено';
    $statuses[] = 'Требует уточнения';
    return $statuses;
}

Изменить config.php

<?php

function get_statuses() {
    $statuses = [
        'Новое',
        'В работе',
        'Завершено'
    ];

    if (file_exists('status_plugin.php')) {
        require_once 'status_plugin.php';
        if (function_exists('extend_statuses')) {
            $statuses = extend_statuses($statuses);
        }
    }

    return $statuses;
}

Задача студентам

  1. Подключить модуль
  2. Добавить новые кнопки для новых статусов
  3. Обновить выпадающий список фильтра
  4. Проверить валидацию на сервере
  5. Добавить запрет перехода из «Завершено» в «Новое»