структура

gruzovozoff/
│
├── app.py
├── database.db (создастся автоматически)
├── requirements.txt
│
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── script.js
│
└── templates/
    ├── base.html
    ├── index.html
    ├── register.html
    ├── login.html
    ├── create_order.html
    ├── orders.html
    └── admin.html

##################################################################################################

from flask import Flask, render_template, request, redirect, session, flash
import sqlite3
import re
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'secret123'

def connect_db():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn

def create_tables():
    conn = connect_db()
    cursor = conn.cursor()

    # Таблицы в 3НФ
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS roles(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        role_name TEXT
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS users(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        full_name TEXT,
        phone TEXT,
        email TEXT,
        login TEXT UNIQUE,
        password TEXT,
        role_id INTEGER,
        FOREIGN KEY(role_id) REFERENCES roles(id)
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS application_statuses(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        status_name TEXT
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS payment_methods(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        method_name TEXT
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS courses(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        course_name TEXT,
        description TEXT,
        price INTEGER
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS applications(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        course_id INTEGER,
        status_id INTEGER,
        payment_method_id INTEGER,
        cargo_type TEXT,
        cargo_weight TEXT,
        cargo_size TEXT,
        from_address TEXT,
        to_address TEXT,
        transport_date TEXT,
        created_at TEXT,
        FOREIGN KEY(user_id) REFERENCES users(id),
        FOREIGN KEY(course_id) REFERENCES courses(id),
        FOREIGN KEY(status_id) REFERENCES application_statuses(id),
        FOREIGN KEY(payment_method_id) REFERENCES payment_methods(id)
    )
    ''')

    cursor.execute('''
    CREATE TABLE IF NOT EXISTS reviews(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        application_id INTEGER,
        review_text TEXT,
        rating INTEGER,
        created_at TEXT,
        FOREIGN KEY(user_id) REFERENCES users(id),
        FOREIGN KEY(application_id) REFERENCES applications(id)
    )
    ''')

    # Начальные данные
    cursor.execute("SELECT * FROM roles")
    if not cursor.fetchall():
        cursor.execute("INSERT INTO roles(role_name) VALUES ('admin')")
        cursor.execute("INSERT INTO roles(role_name) VALUES ('user')")

    cursor.execute("SELECT * FROM application_statuses")
    if not cursor.fetchall():
        cursor.execute("INSERT INTO application_statuses(status_name) VALUES ('Новая')")
        cursor.execute("INSERT INTO application_statuses(status_name) VALUES ('В работе')")
        cursor.execute("INSERT INTO application_statuses(status_name) VALUES ('Выполнена')")
        cursor.execute("INSERT INTO application_statuses(status_name) VALUES ('Отменена')")

    cursor.execute("SELECT * FROM payment_methods")
    if not cursor.fetchall():
        cursor.execute("INSERT INTO payment_methods(method_name) VALUES ('Карта')")
        cursor.execute("INSERT INTO payment_methods(method_name) VALUES ('Наличные')")

    cursor.execute("SELECT * FROM courses")
    if not cursor.fetchall():
        cursor.execute("INSERT INTO courses(course_name, description, price) VALUES ('Стандарт', 'Обычная перевозка', 5000)")

    cursor.execute("SELECT * FROM users WHERE login='admin'")
    if not cursor.fetchone():
        cursor.execute("""
        INSERT INTO users(full_name, phone, email, login, password, role_id)
        VALUES (?, ?, ?, ?, ?, ?)
        """, ('Администратор', '+7(999)-999-99-99', 'admin@mail.ru', 'admin', 'gruzovik2024', 1))

    conn.commit()
    conn.close()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        full_name = request.form['full_name']
        phone = request.form['phone']
        email = request.form['email']
        login = request.form['login']
        password = request.form['password']

        if len(login) < 6:
            flash('Логин минимум 6 символов')
            return redirect('/register')
        if len(password) < 6:
            flash('Пароль минимум 6 символов')
            return redirect('/register')
        if not re.match(r'^[А-Яа-яЁё ]+$', full_name):
            flash('ФИО только кириллица')
            return redirect('/register')
        if not re.match(r'^\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}$', phone):
            flash('Телефон неверный')
            return redirect('/register')
        if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
            flash('Email неверный')
            return redirect('/register')

        conn = connect_db()
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE login=?", (login,))
        if cursor.fetchone():
            flash('Логин занят')
            return redirect('/register')
        cursor.execute("""
        INSERT INTO users(full_name, phone, email, login, password, role_id)
        VALUES (?, ?, ?, ?, ?, ?)
        """, (full_name, phone, email, login, password, 2))
        conn.commit()
        conn.close()
        flash('Регистрация успешна')
        return redirect('/login')
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        login = request.form['login']
        password = request.form['password']
        conn = connect_db()
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE login=? AND password=?", (login, password))
        user = cursor.fetchone()
        conn.close()
        if user:
            session['user_id'] = user['id']
            session['role_id'] = user['role_id']
            session['login'] = user['login']
            if user['login'] == 'admin':
                return redirect('/admin')
            return redirect('/orders')
        flash('Неверный логин или пароль')
        return redirect('/login')
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.clear()
    return redirect('/')

@app.route('/create_order', methods=['GET', 'POST'])
def create_order():
    if 'user_id' not in session:
        return redirect('/login')
    if request.method == 'POST':
        conn = connect_db()
        cursor = conn.cursor()
        cursor.execute("""
        INSERT INTO applications(
            user_id, course_id, status_id, payment_method_id,
            cargo_type, cargo_weight, cargo_size,
            from_address, to_address, transport_date, created_at
        ) VALUES (?, 1, 1, 1, ?, ?, ?, ?, ?, ?, ?)
        """, (
            session['user_id'],
            request.form['cargo_type'],
            request.form['cargo_weight'],
            request.form['cargo_size'],
            request.form['from_address'],
            request.form['to_address'],
            request.form['transport_date'],
            datetime.now()
        ))
        conn.commit()
        conn.close()
        flash('Заявка отправлена')
        return redirect('/orders')
    return render_template('create_order.html')

@app.route('/orders')
def orders():
    if 'user_id' not in session:
        return redirect('/login')
    conn = connect_db()
    cursor = conn.cursor()
    cursor.execute("""
    SELECT applications.*, application_statuses.status_name
    FROM applications
    JOIN application_statuses ON applications.status_id = application_statuses.id
    WHERE user_id=?
    """, (session['user_id'],))
    orders = cursor.fetchall()
    conn.close()
    return render_template('orders.html', orders=orders)

@app.route('/add_review/<int:application_id>', methods=['POST'])
def add_review(application_id):
    if 'user_id' not in session:
        return redirect('/login')
    conn = connect_db()
    cursor = conn.cursor()
    cursor.execute("""
    INSERT INTO reviews(user_id, application_id, review_text, rating, created_at)
    VALUES (?, ?, ?, ?, ?)
    """, (session['user_id'], application_id, request.form['review_text'], request.form['rating'], datetime.now()))
    conn.commit()
    conn.close()
    flash('Отзыв добавлен')
    return redirect('/orders')

@app.route('/admin')
def admin():
    if 'user_id' not in session or session.get('login') != 'admin':
        return redirect('/')
    conn = connect_db()
    cursor = conn.cursor()
    cursor.execute("""
    SELECT applications.*, users.full_name, application_statuses.status_name
    FROM applications
    JOIN users ON applications.user_id = users.id
    JOIN application_statuses ON applications.status_id = application_statuses.id
    """)
    orders = cursor.fetchall()
    conn.close()
    return render_template('admin.html', orders=orders)

@app.route('/change_status/<int:id>', methods=['POST'])
def change_status(id):
    if session.get('login') != 'admin':
        return redirect('/')
    conn = connect_db()
    cursor = conn.cursor()
    cursor.execute("UPDATE applications SET status_id=? WHERE id=?", (request.form['status_id'], id))
    conn.commit()
    conn.close()
    return redirect('/admin')

@app.route('/delete_order/<int:id>')
def delete_order(id):
    if session.get('login') != 'admin':
        return redirect('/')
    conn = connect_db()
    cursor = conn.cursor()
    cursor.execute("DELETE FROM applications WHERE id=?", (id,))
    conn.commit()
    conn.close()
    return redirect('/admin')

if __name__ == '__main__':
    create_tables()
    app.run(debug=True)

##################################################################################################

templates/base.html

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Грузовозофф</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
    <a href="/">Главная</a>
    {% if session.get('user_id') %}
        <a href="/create_order">Создать заявку</a>
        <a href="/orders">Мои заявки</a>
        {% if session.get('login') == 'admin' %}
            <a href="/admin">Админ панель</a>
        {% endif %}
        <a href="/logout">Выход</a>
    {% else %}
        <a href="/register">Регистрация</a>
        <a href="/login">Авторизация</a>
    {% endif %}
</nav>
<div class="container">
    {% with messages = get_flashed_messages() %}
        {% if messages %}
            {% for message in messages %}
                <div class="flash">{{ message }}</div>
            {% endfor %}
        {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
</body>
</html>

##################################################################################################

templates/index.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h1>Грузовозофф</h1>
    <p>Быстрые и надёжные грузоперевозки по всей России</p>
</div>
{% endblock %}

##################################################################################################

templates/register.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h2>Регистрация</h2>
    <form method="POST">
        <label>ФИО:</label>
        <input type="text" name="full_name" required pattern="[А-Яа-яЁё ]+" placeholder="Иванов Иван Иванович">

        <label>Телефон:</label>
        <input type="tel" name="phone" required pattern="\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}" placeholder="+7(999)-999-99-99">

        <label>Email:</label>
        <input type="email" name="email" required placeholder="ivan@mail.ru">

        <label>Логин (мин 6 символов):</label>
        <input type="text" name="login" required minlength="6">

        <label>Пароль (мин 6 символов):</label>
        <input type="password" name="password" required minlength="6">

        <button type="submit">Зарегистрироваться</button>
    </form>
    <p>Уже есть аккаунт? <a href="/login">Войти</a></p>
</div>
{% endblock %}

##################################################################################################

templates/login.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h2>Авторизация</h2>
    <form method="POST">
        <label>Логин:</label>
        <input type="text" name="login" required>

        <label>Пароль:</label>
        <input type="password" name="password" required>

        <button type="submit">Войти</button>
    </form>
    <p>Нет аккаунта? <a href="/register">Зарегистрироваться</a></p>
</div>
{% endblock %}

##################################################################################################

templates/create_order.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h2>Создание заявки на перевозку</h2>
    <form method="POST">
        <label>Тип груза:</label>
        <select name="cargo_type" id="cargo_type" required>
            <option value="Обычный">Обычный</option>
            <option value="Хрупкий">Хрупкий</option>
            <option value="Опасный">Опасный</option>
            <option value="Мусор">Мусор</option>
            <option value="Продукты">Продукты</option>
        </select>

        <label>Вес груза (кг):</label>
        <input type="text" name="cargo_weight" required placeholder="например: 500">

        <label>Габариты (Д×Ш×В, см):</label>
        <input type="text" name="cargo_size" required placeholder="например: 200×150×100">

        <label>Адрес отправления:</label>
        <input type="text" name="from_address" required placeholder="г. Москва, ул. Примерная, д.1">

        <label>Адрес назначения:</label>
        <input type="text" name="to_address" required placeholder="г. Санкт-Петербург, ул. Невский пр., д.10">

        <label>Дата перевозки:</label>
        <input type="date" name="transport_date" required>

        <button type="submit">Отправить заявку</button>
    </form>
</div>
{% endblock %}

##################################################################################################

templates/orders.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h2>Мои заявки</h2>
    {% if orders %}
        {% for order in orders %}
        <div class="card" style="margin-top: 20px;">
            <h3>Заявка №{{ order.id }}</h3>
            <p><strong>Тип груза:</strong> {{ order.cargo_type }}</p>
            <p><strong>Вес:</strong> {{ order.cargo_weight }} кг</p>
            <p><strong>Габариты:</strong> {{ order.cargo_size }} см</p>
            <p><strong>Откуда:</strong> {{ order.from_address }}</p>
            <p><strong>Куда:</strong> {{ order.to_address }}</p>
            <p><strong>Дата перевозки:</strong> {{ order.transport_date }}</p>
            <p><strong>Статус:</strong> 
                <span style="color: 
                    {% if order.status_name == 'Новая' %}blue
                    {% elif order.status_name == 'В работе' %}orange
                    {% elif order.status_name == 'Выполнена' %}green
                    {% elif order.status_name == 'Отменена' %}red
                    {% endif %}">
                    {{ order.status_name }}
                </span>
            </p>
            <p><strong>Дата создания:</strong> {{ order.created_at }}</p>

            {% if order.status_name == 'Выполнена' %}
            <hr>
            <h4>Оставить отзыв</h4>
            <form method="POST" action="/add_review/{{ order.id }}">
                <label>Оценка:</label>
                <select name="rating" required>
                    <option value="1">1 - Ужасно</option>
                    <option value="2">2 - Плохо</option>
                    <option value="3">3 - Нормально</option>
                    <option value="4">4 - Хорошо</option>
                    <option value="5">5 - Отлично</option>
                </select>
                <label>Отзыв:</label>
                <textarea name="review_text" rows="3" required placeholder="Напишите ваш отзыв..."></textarea>
                <button type="submit">Отправить отзыв</button>
            </form>
            {% endif %}
        </div>
        {% endfor %}
    {% else %}
        <p>У вас пока нет заявок. <a href="/create_order">Создать заявку</a></p>
    {% endif %}
</div>
{% endblock %}

##################################################################################################

templates/admin.html

{% extends "base.html" %}
{% block content %}
<div class="card">
    <h2>Админ панель - управление заявками</h2>
    
    {% if orders %}
        <style>
            .admin-table {
                width: 100%;
                border-collapse: collapse;
                background: white;
            }
            .admin-table th, .admin-table td {
                border: 1px solid #ddd;
                padding: 12px;
                text-align: left;
            }
            .admin-table th {
                background: #007bff;
                color: white;
            }
            .admin-table tr:nth-child(even) {
                background: #f9f9f9;
            }
            .admin-table tr:hover {
                background: #f1f1f1;
            }
            .status-select {
                padding: 5px;
                margin-right: 10px;
            }
            .delete-btn {
                background: #dc3545;
                color: white;
                padding: 5px 10px;
                text-decoration: none;
                border-radius: 3px;
            }
            .delete-btn:hover {
                background: #c82333;
            }
            .update-btn {
                background: #28a745;
                color: white;
                padding: 5px 10px;
                border: none;
                border-radius: 3px;
                cursor: pointer;
            }
            .update-btn:hover {
                background: #218838;
            }
        </style>

        <table class="admin-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Клиент</th>
                    <th>Тип груза</th>
                    <th>Вес</th>
                    <th>Откуда</th>
                    <th>Куда</th>
                    <th>Дата</th>
                    <th>Статус</th>
                    <th>Действия</th>
                </tr>
            </thead>
            <tbody>
                {% for order in orders %}
                <tr>
                    <td>{{ order.id }}</td>
                    <td>{{ order.full_name }}</td>
                    <td>{{ order.cargo_type }}</td>
                    <td>{{ order.cargo_weight }} кг</td>
                    <td>{{ order.from_address }}</td>
                    <td>{{ order.to_address }}</td>
                    <td>{{ order.transport_date }}</td>
                    <td>
                        <form method="POST" action="/change_status/{{ order.id }}" style="display: flex; gap: 5px;">
                            <select name="status_id" class="status-select">
                                <option value="1" {% if order.status_id == 1 %}selected{% endif %}>Новая</option>
                                <option value="2" {% if order.status_id == 2 %}selected{% endif %}>В работе</option>
                                <option value="3" {% if order.status_id == 3 %}selected{% endif %}>Выполнена</option>
                                <option value="4" {% if order.status_id == 4 %}selected{% endif %}>Отменена</option>
                            </select>
                            <button type="submit" class="update-btn">Обновить</button>
                        </form>
                    </td>
                    <td>
                        <a href="/delete_order/{{ order.id }}" class="delete-btn" onclick="return confirm('Удалить заявку №{{ order.id }}?')">Удалить</a>
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <p>Заявок пока нет.</p>
    {% endif %}
</div>
{% endblock %}

##################################################################################################

body {
    margin: 0;
    padding: 0;
    font-family: Arial;
    background: #f4f4f4;
}
.container {
    width: 90%;
    max-width: 1000px;
    margin: auto;
    padding: 20px;
}
nav {
    background: #222;
    padding: 15px;
}
nav a {
    color: white;
    text-decoration: none;
    margin-right: 15px;
}
form {
    background: white;
    padding: 20px;
    border-radius: 10px;
}
input, select, textarea {
    width: 100%;
    padding: 12px;
    margin-top: 10px;
    margin-bottom: 15px;
}
button {
    background: #007bff;
    color: white;
    border: none;
    padding: 12px 20px;
    cursor: pointer;
    transition: 0.3s;
}
button:hover {
    background: #0056b3;
}
.card {
    background: white;
    padding: 20px;
    margin-bottom: 20px;
    border-radius: 10px;
}
.flash {
    background: #28a745;
    color: white;
    padding: 10px;
    margin-bottom: 15px;
}
@media (max-width: 390px) {
    .container { width: 95%; }
    nav a { display: block; margin-bottom: 10px; }
}

##################################################################################################

static/js/script.js

document.addEventListener('DOMContentLoaded', function() {
    const cargoType = document.getElementById('cargo_type');
    if (cargoType) {
        cargoType.addEventListener('change', function() {
            if (this.value === 'Мусор') {
                alert('Стоимость будет увеличена из-за утилизации');
            }
        });
    }
});

##################################################################################################

pgAdmin

CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    role_name VARCHAR(50) NOT NULL
);

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    full_name VARCHAR(255) NOT NULL,
    phone VARCHAR(20) NOT NULL,
    email VARCHAR(100) NOT NULL,
    login VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    role_id INTEGER NOT NULL REFERENCES roles(id)
);

CREATE TABLE application_statuses (
    id SERIAL PRIMARY KEY,
    status_name VARCHAR(50) NOT NULL
);

CREATE TABLE payment_methods (
    id SERIAL PRIMARY KEY,
    method_name VARCHAR(50) NOT NULL
);

CREATE TABLE courses (
    id SERIAL PRIMARY KEY,
    course_name VARCHAR(100) NOT NULL,
    description TEXT,
    price INTEGER NOT NULL
);

CREATE TABLE applications (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    course_id INTEGER NOT NULL REFERENCES courses(id),
    status_id INTEGER NOT NULL REFERENCES application_statuses(id),
    payment_method_id INTEGER NOT NULL REFERENCES payment_methods(id),
    cargo_type VARCHAR(100) NOT NULL,
    cargo_weight VARCHAR(50) NOT NULL,
    cargo_size VARCHAR(100) NOT NULL,
    from_address TEXT NOT NULL,
    to_address TEXT NOT NULL,
    transport_date DATE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE reviews (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    application_id INTEGER NOT NULL REFERENCES applications(id),
    review_text TEXT NOT NULL,
    rating INTEGER CHECK (rating >= 1 AND rating <= 5),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

##################################################################################################

Как запустить

# 1. Создайте папку gruzovozoff
# 2. Скопируйте все файлы по структуре
# 3. Откройте терминал в этой папке
# 4. Создайте виртуальное окружение
python3 -m venv название
source название/bin/activate
# 5. Установите Flask
pip install -r requirements.txt
# 6. Запустите приложение
python app.py