#import "@local/nure:0.1.0": *

#show: pz-lb.with(
  title: [Створення WEB-застосунків за допомогою PHP],
  subject: "СМП",
  doctype: "ЛБ",
  worknumber: 3,
  edu_program: "ПЗПІ",
  university: "ХНУРЕ",
  mentors: (
    (
      name: "Сокорчук І. П.",
      degree: "Старший викладач кафедри ПІ",
      gender: "m",
    ),
  ),
  authors: (
    (
      name: "Ситник Є. С.",
      course: 2,
      edu: "ПЗПІ",
      gender: "m",
      group: "23-2",
    ),
  ),
)

#v(-spacing)
== Історія змін
#figure(
  table(
    align: left,
    columns: (auto, 1fr, 1fr, 3fr),
    table.header([№], [Дата], [Версія звіту], [Опис змін та виправлень]),
    [1], [03.06.2025], [0.1], [Створено звіт],
  ),
)

== Мета роботи
Лабораторна робота полягає у розробці веб-застосунку "Продовольчий магазин Весна" засобами мови програмування PHP з використанням HTML, CSS та JavaScript для створення інтерактивного інтерфейсу користувача.

== Хід роботи
#v(-spacing)
=== Архітектура веб-застосунку

Для реалізації веб-застосунку було обрано модульний підхід з розділенням логіки на окремі файли:

==== Структура файлів:

- *index.php* -- головна сторінка застосунку з логотипом магазину;
- *items.php* -- сторінка каталогу товарів з можливістю додавання до кошика;
- *cart.php* -- сторінка кошика з переглядом обраних товарів та управлінням;
- *handle_cart.php* -- обробник AJAX-запитів для роботи з кошиком;
- *DB.php* -- клас для роботи з базою даних SQLite;
- *style.css* -- стилі для оформлення веб-сторінок.

==== Основні компоненти:

- *Клас DB* -- забезпечує взаємодію з базою даних SQLite, включаючи методи для роботи з товарами, кошиком та налаштуваннями користувача;
- *Клас DbException* -- спеціалізований виняток для обробки помилок бази даних;
- *Сесійна система* -- для збереження стану користувача між сторінками.

=== Структура бази даних

Веб-застосунок використовує базу даних SQLite з трьома основними таблицями:

==== Таблиця settings:
- *name* (TEXT) -- ім'я користувача;
- *age* (TEXT) -- вік користувача.

==== Таблиця items:
- *id* (INTEGER PRIMARY KEY) -- унікальний ідентифікатор товару;
- *name* (TEXT NOT NULL) -- назва товару;
- *price* (REAL NOT NULL) -- ціна товару.

==== Таблиця cart:
- *id* (INTEGER NOT NULL UNIQUE) -- ідентифікатор товару;
- *count* (INTEGER NOT NULL) -- кількість товару в кошику;
- *FOREIGN KEY* -- зв'язок з таблицею items.

=== Функціональні можливості

==== Головна сторінка (index.php):
- Відображення логотипу магазину;
- Навігаційне меню з кількістю товарів у кошику;
- Привітання користувача за ім'ям;
- Підвал з інформацією про магазин.

==== Каталог товарів (items.php):
- Відображення списку доступних товарів з цінами;
- Форма для вибору кількості товару;
- Можливість додавання товарів до кошика;
- Валідація кількості товару (від 0 до 100 одиниць);
- Автоматичне оновлення лічильника кошика.

==== Кошик покупок (cart.php):
- Перегляд обраних товарів з детальною інформацією;
- Відображення кількості, ціни за одиницю та загальної вартості;
- Можливість видалення окремих товарів з кошика;
- Функція повного очищення кошика;
- Підрахунок загальної суми покупки;
- Кнопка "Сплатити" для завершення покупки.

==== Обробка запитів (handle_cart.php):
- Обробка POST-запитів для додавання товарів до кошика;
- Обробка DELETE-запитів для видалення товарів;
- Підтримка AJAX для асинхронного оновлення кошика;
- Валідація вхідних даних та обробка помилок.

=== Технічні особливості

==== Безпека:
- Використання підготовлених запитів (prepared statements) для запобігання SQL-ін'єкціям;
- Функція "htmlspecialchars()" для запобігання XSS-атакам;
- Валідація та фільтрація користувацьких даних через "filter_input()";
- Обробка винятків для коректної роботи з помилками.

==== Користувацький інтерфейс:
- Адаптивний дизайн з використанням CSS;
- Інтуїтивна навігація між сторінками;
- Зручні форми для взаємодії з користувачем.

==== База даних:
- Автоматичне створення таблиць при першому запуску;
- Заповнення початковими даними;

=== Методи класу DB

Клас DB містить наступні основні методи:

- *retrieve_user()* -- отримання інформації про користувача;
- *update_user()* -- оновлення профілю користувача;
- *get_items()* -- отримання списку всіх товарів;
- *get_item_by_id()* -- отримання конкретного товару за ID;
- *get_cart()* -- отримання товарів у кошику з детальною інформацією;
- *get_cart_count()* -- підрахунок загальної кількості товарів у кошику;
- *get_cart_total()* -- підрахунок загальної вартості кошика;
- *add_to_cart()* -- додавання товару до кошика або оновлення кількості;
- *remove_from_cart()* -- видалення товару з кошика;
- *empty_cart()* -- повне очищення кошика.

== Висновки

Під час виконання даної лабораторної роботи було успішно розроблено веб-застосунок інтернет-магазину з використанням сучасних веб-технологій. Зокрема, було освоєно:

- створення багатосторінкових веб-застосунків на PHP;
- роботу з базами даних SQLite через PDO з дотриманням принципів безпеки;
- реалізацію сесійної системи для збереження стану користувача;
- створення інтерактивного користувацького інтерфейсу з використанням HTML та CSS;
- валідацію та фільтрацію користувацьких даних.

Застосунок демонструє практичне застосування технологій веб-розробки для створення реальних рішень.

#set raw(tab-size: 8)
#show raw.where(block: true): code => {
  set text(11pt, top-edge: 1em, bottom-edge: 0em)
  set par(leading: 0.17em)

  grid(
    columns: (auto, auto),
    column-gutter: 1em,
    row-gutter: 0.17em,
    align: (right, raw.align),
    ..for line in code.lines {
      (
        text(fill: gray)[#line.number],
        {
          set text(weight: "semibold")
          line.body
        },
      )
    },
  )
}
#show: appendices_style
= Відеозапис
Відеозапис презентації результатів лабораторної роботи: https://youtu.be/Gils7poMkgk

*Хронологічний опис відеозапису:*

00:00 -- Вступ та загальний опис роботи

00:30 -- Структура веб-застосунку

01:14 -- Робота з базою даних та сесіями

01:43 -- Обробка запитів користувача

03:56 -- Демонстрація роботи веб-застосунку

= Програмний код

== Головна сторінка (index.php)
GitHub репозиторій: https://github.com/NureSytnykYehor/smp-pzpi-23-2-sytnyk-yehor/blob/main/Lab3/smp-pzpi-23-2-sytnyk-yehor-lab3/index.php
```
<?php
session_start();
require_once 'DB.php';

$db = new DB('shop.db');

if (!isset($_SESSION['user'])) {
    $_SESSION['user'] = $db->retrieve_user();
}

$cart_count = $db->get_cart_count();
?>

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <title>Головна сторінка</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>Продовольчий магазин "Весна"</h1>
        <h3> Добрий день <?php echo $_SESSION['user']['name'] ?> </h3>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
    </header>

    <div class="container" style="display: flex; flex-direction: column; align-items: center;">
        <img src="logo.png" alt="logo" style="width: 90%;">
    </div>

    <footer>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
        <p>&copy; <?php echo date("Y"); ?> ТОВ "Весна". Усі права захищені.</p>
    </footer>
</body>
</html>
```

== Каталог товарів (items.php)
GitHub репозиторій: https://github.com/NureSytnykYehor/smp-pzpi-23-2-sytnyk-yehor/blob/main/Lab3/smp-pzpi-23-2-sytnyk-yehor-lab3/items.php
```
<?php
session_start();
require_once 'DB.php';

$db = new DB('shop.db');

if (!isset($_SESSION['user'])) {
    $_SESSION['user'] = $db->retrieve_user();
}

$items = $db->get_items();
$cart_count = $db->get_cart_count();
?>

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <title>Сторінка товарів</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>Продовольчий магазин "Весна"</h1>
        <h3> Добрий день <?php echo $_SESSION['user']['name'] ?> </h3>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
    </header>

    <div class="container">
        <h2>Доступні товари</h2>
        <div class="product-list">
            <?php foreach ($items as $item): ?>
                <div>
                    <h2><?php echo htmlspecialchars($item['name']); ?></h2>
                    <h3>Ціна: <?php echo number_format($item['price'], 2); ?> грн</h3>

                    <form action="handle_cart.php" method="POST">
                        <input type="hidden" name="product_id" value="<?php echo htmlspecialchars($item['id']); ?>">
                        <label for="quantity_<?php echo htmlspecialchars($item['id']); ?>">Кількість:</label>
                        <input type="number" id="quantity_<?php echo htmlspecialchars($item['id']); ?>" name="quantity" value="0" min="0" max="100">
                        <button type="submit">Купити</button>
                    </form>
                </div>
            <?php endforeach; ?>
        </div>
    </div>

    <footer>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
        <p>&copy; <?php echo date("Y"); ?> ТОВ "Весна". Усі права захищені.</p>
    </footer>
</body>
</html>
```

== Кошик покупок (cart.php)
GitHub репозиторій: https://github.com/NureSytnykYehor/smp-pzpi-23-2-sytnyk-yehor/blob/main/Lab3/smp-pzpi-23-2-sytnyk-yehor-lab3/cart.php
```
<?php
session_start();
require_once 'DB.php';

$db = new DB("shop.db");

if (!isset($_SESSION['user'])) {
    $_SESSION['user'] = $db->retrieve_user();
}

$cart_items = $db->get_cart();
$cart_total = $db->get_cart_total();
$cart_count = $db->get_cart_count();
?>

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <title>Кошик</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>Продовольчий магазин "Весна"</h1>
        <h3> Добрий день <?php echo $_SESSION['user']['name'] ?> </h3>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
    </header>

    <div class="container">
        <?php if (empty($cart_items)): ?>
            <div style="display: flex; align-items: center; justify-content: space-evenly;">
                <h3>Ваш кошик порожній <a href="items.php">Перейти до покупок</a> </h3>
            </div>
        <?php else: ?>
            <div style="display: flex; align-items: center; justify-content: space-evenly;">
                <h3>Ваш кошик</h3>
                <h3 class="cart-summary">
                    Загальна сума: <?php echo number_format($cart_total, 2); ?> грн
                </h3>

                <button type="submit">Сплатити</button>

                <button onclick="fetch('handle_cart.php', {'method': 'DELETE'}).then(_ => { location.reload(); });">Очистити</button>
            </div>

            <div class="product-list">
                <?php foreach ($cart_items as $item): ?>
                    <div>
                        <h2>
                            <?php echo htmlspecialchars($item['name']); ?>
                            <br>
                            <?php echo htmlspecialchars($item['count']); ?> шт.
                        </h2>

                        <span>Ціна за одиницю: <?php echo number_format($item['price'], 2); ?> грн</span>
                        <br>
                        <span>Загальна ціна: <?php echo number_format($item['total_price'], 2); ?> грн</span>

                        <br><br>

                        <button
                            style="width: 100%;"
                            onclick="
                            fetch(
                                'handle_cart.php?product_id=<?php echo htmlspecialchars($item['id']); ?>',
                                { 'method': 'DELETE' }
                            ).then(_ => { location.reload(); });">
                            Видалити
                        </button>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>
    </div>

    <footer>
        <nav>
            <a href="index.php">Головна</a>
            |
            <a href="items.php">Товари</a>
            |
            <a href="cart.php">Кошик (<?php echo $cart_count ?? 0; ?>)</a>
        </nav>
        <p>&copy; <?php echo date("Y"); ?> ТОВ "Весна". Усі права захищені.</p>
    </footer>
</body>
</html>
```

== Обробник кошика (handle_cart.php)
GitHub репозиторій: https://github.com/NureSytnykYehor/smp-pzpi-23-2-sytnyk-yehor/blob/main/Lab3/smp-pzpi-23-2-sytnyk-yehor-lab3/handle_cart.php
```
<?php
session_start();
require_once 'DB.php';

$db_path = 'shop.db';
$db = new DB($db_path);

switch ($_SERVER['REQUEST_METHOD']) {
    case 'POST':
        $product_id = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
        $quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);

        if ($product_id !== false && $product_id !== null && $quantity !== false && $quantity !== null) {
            try {
                $db->add_to_cart($product_id, $quantity);
            } catch (DbException $e) {
                error_log("Cart handling error: " . $e->getMessage());
            }
        }
        header('Location: items.php');
        break;

    case 'DELETE':
        $product_id = filter_input(INPUT_GET, 'product_id', FILTER_VALIDATE_INT);

        if ($product_id === null) {
            try {
                $db->empty_cart();
            } catch (DbException $e) {
                error_log("Cart handling error: " . $e->getMessage());
            }
        } else if ($product_id !== false) {
            try {
                $db->remove_from_cart($product_id);
            } catch (DbException $e) {
                error_log("Cart handling error: " . $e->getMessage());
            }
        }

        header('Location: cart.php');
        break;

    default:
        break;
}

exit();
```

== Клас для роботи з базою даних (DB.php)
GitHub репозиторій: https://github.com/NureSytnykYehor/smp-pzpi-23-2-sytnyk-yehor/blob/main/Lab3/smp-pzpi-23-2-sytnyk-yehor-lab3/DB.php
```
<?php

class DbException extends Exception {}

class DB
{
    private $pdo;

    /**
     * Initializes database
     *
     * @param string $db_path
     * @throws DbException If there's a database error.
     */
    public function __construct($db_path)
    {
        try {
            $this->pdo = new PDO("sqlite:" . $db_path);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $e) {
            throw new DbException("Connection to DB failed.\nCaused by: " . $e->getMessage());
        }

        // Ініціалізація таблиць та початкових даних
        try {
            $this->pdo->exec("
                CREATE TABLE IF NOT EXISTS settings (
                    name TEXT,
                    age TEXT
                );
            ");
            if ($this->pdo->query("SELECT COUNT(*) FROM settings;")->fetchColumn() == 0) {
                $this->pdo->exec("INSERT INTO settings (name, age) VALUES ('user', 0);");
            }
        } catch (PDOException $e) {
            throw new DbException("Error initialising settings table.\nCaused by: " . $e->getMessage());
        }

        try {
            $this->pdo->exec("
                CREATE TABLE IF NOT EXISTS items (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    price REAL NOT NULL
                );
            ");
            if ($this->pdo->query("SELECT COUNT(id) FROM items;")->fetchColumn() == 0) {
                $this->pdo->exec("
                    INSERT INTO items (name, price) VALUES ('Молоко пастеризоване', 32.50);
                    INSERT INTO items (name, price) VALUES ('Хліб чорний', 18.00);
                    INSERT INTO items (name, price) VALUES ('Сир білий', 85.00);
                    INSERT INTO items (name, price) VALUES ('Сметана 20%', 45.80);
                    INSERT INTO items (name, price) VALUES ('Кефір 1%', 28.50);
                    INSERT INTO items (name, price) VALUES ('Вода газована', 25.00);
                    INSERT INTO items (name, price) VALUES ('Печиво \"Весна\"', 42.30);
                    INSERT INTO items (name, price) VALUES ('Масло вершкове', 125.00);
                    INSERT INTO items (name, price) VALUES ('Йогурт натуральний', 38.90);
                    INSERT INTO items (name, price) VALUES ('Сік апельсиновий', 55.00);
                ");
            }
        } catch (PDOException $e) {
            throw new DbException("Error initialising items table.\nCaused by: " . $e->getMessage());
        }

        try {
            $this->pdo->exec("
                CREATE TABLE IF NOT EXISTS cart (
                    id INTEGER NOT NULL UNIQUE,
                    count INTEGER NOT NULL,
                    FOREIGN KEY(id) REFERENCES items(id) ON DELETE CASCADE
                );
            ");
        } catch (PDOException $e) {
            throw new DbException("Error initialising cart table.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Retrieve user information from the database
     *
     * @return array
     * @throws DbException If there's a database error.
     */
    public function retrieve_user(): array
    {
        try {
            $stmt = $this->pdo->query("SELECT name, age FROM settings;");
            return $stmt->fetch(PDO::FETCH_ASSOC);
        } catch (PDOException $e) {
            throw new DbException("Error retrieving user info.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Fetches all items from the database.
     *
     * @return array[]
     * @throws DbException If there's a database error.
     */
    public function get_items(): array
    {
        try {
            $stmt = $this->pdo->query("SELECT id, name, price FROM items ORDER BY id;");
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $e) {
            throw new DbException("Error retrieving data from the items table.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Fetches all items in the cart from the database.
     *
     * @return array[]
     * @throws DbException If there's a database error.
     */
    public function get_cart(): array
    {
        try {
            $stmt = $this->pdo->query(
                "SELECT
                    cart.id,
                    items.name,
                    items.price,
                    cart.count,
                    ROUND(items.price * cart.count, 2) as total_price
                FROM cart
                INNER JOIN items ON cart.id = items.id
                ORDER BY cart.id;"
            );
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $e) {
            throw new DbException("Error retrieving cart items.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Get total items count in cart
     *
     * @return int
     * @throws DbException If there's a database error.
     */
    public function get_cart_count(): int
    {
        try {
            $stmt = $this->pdo->query("SELECT COALESCE(SUM(count), 0) FROM cart;");
            return (int)$stmt->fetchColumn();
        } catch (PDOException $e) {
            throw new DbException("Error getting cart count.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Get total price of all items in cart
     *
     * @return float
     * @throws DbException If there's a database error.
     */
    public function get_cart_total(): float
    {
        try {
            $stmt = $this->pdo->query(
                "SELECT COALESCE(SUM(items.price * cart.count), 0.0)
                FROM cart
                INNER JOIN items ON cart.id = items.id;"
            );
            return (float)$stmt->fetchColumn();
        } catch (PDOException $e) {
            throw new DbException("Error calculating cart total.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Add item to the cart or update its quantity.
     *
     * @param int $id
     * @param int $count
     *
     * @return bool
     * @throws DbException If there's a database error or item doesn't exist.
     */
    public function add_to_cart($id, $count): bool
    {
        try {
            // Check if the item exists
            $item = $this->get_item_by_id($id);
            if (!$item) {
                throw new DbException("Item with ID $id does not exist.");
            }

            // If count is 0 or less, remove the item from the cart
            if ($count <= 0) {
                return $this->remove_from_cart($id);
            }

            // Insert or update the cart item
            $stmt = $this->pdo->prepare(
                "INSERT INTO cart (id, count)
                VALUES (:id, :count)
                ON CONFLICT(id) DO UPDATE SET
                    count = excluded.count;"
            );
            return $stmt->execute(['id' => $id, 'count' => $count]);
        } catch (PDOException $e) {
            throw new DbException("Error adding/updating item in the cart.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Empty the cart
     *
     * @return bool
     * @throws DbException If there's a database error.
     */
    public function empty_cart(): bool
    {
        try {
            $stmt = $this->pdo->prepare("DELETE FROM cart");
            return $stmt->execute();
        } catch (PDOException $e) {
            throw new DbException("Error removing item from the cart.\nCaused by: " . $e->getMessage());
        }
    }

    /**
     * Remove an item from the cart
     *
     * @param int $id
     *
     * @return bool
     * @throws DbException If there's a database error.
     */
    public function remove_from_cart($id): bool
    {
        try {
            $stmt = $this->pdo->prepare("DELETE FROM cart WHERE id = :id");
            return $stmt->execute(['id' => $id]);
        } catch (PDOException $e) {
            throw new DbException("Error removing item from the cart.\nCaused by: " . $e->getMessage());
        }
    }
}
```
