sqlalchemy-connection 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_connection-2.0.1.dist-info/METADATA +26 -0
- sqlalchemy_connection-2.0.1.dist-info/RECORD +33 -0
- sqlalchemy_connection-2.0.1.dist-info/WHEEL +5 -0
- sqlalchemy_connection-2.0.1.dist-info/entry_points.txt +2 -0
- sqlalchemy_connection-2.0.1.dist-info/top_level.txt +1 -0
- sqlalchemy_connector/__init__.py +3 -0
- sqlalchemy_connector/_builder.py +425 -0
- sqlalchemy_connector/cli.py +200 -0
- sqlalchemy_connector/real_generator.py +2908 -0
- sqlalchemy_connector/templates/admin_cart_html_template.html +372 -0
- sqlalchemy_connector/templates/admin_html_template.html +364 -0
- sqlalchemy_connector/templates/admin_users_html_template.html +82 -0
- sqlalchemy_connector/templates/app_template.py +434 -0
- sqlalchemy_connector/templates/base_html_template.html +100 -0
- sqlalchemy_connector/templates/cart_html_template.html +103 -0
- sqlalchemy_connector/templates/catalog_html_template.html +98 -0
- sqlalchemy_connector/templates/checkout_html_template.html +70 -0
- sqlalchemy_connector/templates/dashboard_html_template.html +121 -0
- sqlalchemy_connector/templates/index_html_template.html +91 -0
- sqlalchemy_connector/templates/login_html_template.html +59 -0
- sqlalchemy_connector/templates/models_template.py +65 -0
- sqlalchemy_connector/templates/new_request_html_template.html +49 -0
- sqlalchemy_connector/templates/orders_html_template.html +65 -0
- sqlalchemy_connector/templates/product_form_html_template.html +142 -0
- sqlalchemy_connector/templates/product_html_template.html +131 -0
- sqlalchemy_connector/templates/profile_html_template.html +104 -0
- sqlalchemy_connector/templates/register_html_template.html +183 -0
- sqlalchemy_connector/templates/reviews_html_template.html +104 -0
- sqlalchemy_connector/templates/service_detail_html_template.html +67 -0
- sqlalchemy_connector/templates/service_form_html_template.html +86 -0
- sqlalchemy_connector/templates/services_html_template.html +47 -0
- sqlalchemy_connector/templates/slider_js_template.js +99 -0
- sqlalchemy_connector/templates/style_css_template.css +502 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Каталог{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
|
7
|
+
<h3 class="mb-0">🛍️ Каталог товаров</h3>
|
|
8
|
+
{% if session.role == 'admin' %}
|
|
9
|
+
<a href="{{ url_for('admin_product_add') }}" class="btn btn-success btn-sm">➕ Добавить товар</a>
|
|
10
|
+
{% endif %}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Поиск и фильтр -->
|
|
14
|
+
<form method="GET" class="row g-2 mb-4">
|
|
15
|
+
<div class="col-sm-6 col-md-4">
|
|
16
|
+
<input type="text" class="form-control" name="search"
|
|
17
|
+
value="{{ search or '' }}" placeholder="Поиск по названию...">
|
|
18
|
+
</div>
|
|
19
|
+
<div class="col-sm-4 col-md-3">
|
|
20
|
+
<select class="form-select" name="sort">
|
|
21
|
+
<option value="" {% if not sort %}selected{% endif %}>Сортировка</option>
|
|
22
|
+
<option value="price_asc" {% if sort == 'price_asc' %}selected{% endif %}>Цена ↑</option>
|
|
23
|
+
<option value="price_desc" {% if sort == 'price_desc' %}selected{% endif %}>Цена ↓</option>
|
|
24
|
+
<option value="name" {% if sort == 'name' %}selected{% endif %}>По названию</option>
|
|
25
|
+
</select>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="col-auto">
|
|
28
|
+
<button type="submit" class="btn btn-primary">🔍 Найти</button>
|
|
29
|
+
<a href="{{ url_for('catalog') }}" class="btn btn-outline-secondary">Сбросить</a>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
{% if products %}
|
|
34
|
+
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4 g-3">
|
|
35
|
+
{% for product in products %}
|
|
36
|
+
<div class="col animate-fade-up">
|
|
37
|
+
<div class="card h-100 product-card border-0 shadow-sm">
|
|
38
|
+
<a href="{{ url_for('product_detail', product_id=product.id) }}" class="text-decoration-none">
|
|
39
|
+
{% if product.image %}
|
|
40
|
+
<img src="{{ url_for('static', filename='uploads/products/' ~ product.image) }}"
|
|
41
|
+
class="card-img-top" alt="{{ product.name }}"
|
|
42
|
+
style="height:180px;object-fit:cover;">
|
|
43
|
+
{% else %}
|
|
44
|
+
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
|
|
45
|
+
style="height:180px;font-size:48px;">🛍️</div>
|
|
46
|
+
{% endif %}
|
|
47
|
+
</a>
|
|
48
|
+
<div class="card-body d-flex flex-column">
|
|
49
|
+
<h6 class="card-title text-truncate-2 mb-1">
|
|
50
|
+
<a href="{{ url_for('product_detail', product_id=product.id) }}"
|
|
51
|
+
class="text-decoration-none text-dark">{{ product.name }}</a>
|
|
52
|
+
</h6>
|
|
53
|
+
{% if product.description %}
|
|
54
|
+
<p class="card-text text-muted small text-truncate-2 mb-2">{{ product.description }}</p>
|
|
55
|
+
{% endif %}
|
|
56
|
+
{% if product.stock is defined %}
|
|
57
|
+
<small class="text-muted mb-2">На складе: {{ product.stock }} шт.</small>
|
|
58
|
+
{% endif %}
|
|
59
|
+
<div class="mt-auto d-flex justify-content-between align-items-center">
|
|
60
|
+
<strong class="text-success fs-5">{{ product.price }} ₽</strong>
|
|
61
|
+
{% if session.user_id %}
|
|
62
|
+
<form method="POST" action="{{ url_for('cart_add', product_id=product.id) }}">
|
|
63
|
+
<button type="submit" class="btn btn-primary btn-sm">В корзину</button>
|
|
64
|
+
</form>
|
|
65
|
+
{% else %}
|
|
66
|
+
<a href="{{ url_for('login') }}" class="btn btn-outline-primary btn-sm">Войти</a>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
{% if session.role == 'admin' %}
|
|
71
|
+
<div class="card-footer bg-transparent border-0 pt-0">
|
|
72
|
+
<div class="d-flex gap-1">
|
|
73
|
+
<a href="{{ url_for('admin_product_edit', product_id=product.id) }}"
|
|
74
|
+
class="btn btn-outline-secondary btn-sm flex-grow-1">✏️</a>
|
|
75
|
+
<form method="POST" action="{{ url_for('admin_product_delete', product_id=product.id) }}"
|
|
76
|
+
onsubmit="return confirm('Удалить товар?')" class="flex-grow-1">
|
|
77
|
+
<button type="submit" class="btn btn-outline-danger btn-sm w-100">🗑</button>
|
|
78
|
+
</form>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
{% endif %}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{% endfor %}
|
|
85
|
+
</div>
|
|
86
|
+
{% else %}
|
|
87
|
+
<div class="text-center py-5 text-muted">
|
|
88
|
+
<div class="fs-1">📦</div>
|
|
89
|
+
<h5 class="mt-3">Товары не найдены</h5>
|
|
90
|
+
{% if search %}
|
|
91
|
+
<p>По запросу «{{ search }}» ничего не найдено</p>
|
|
92
|
+
<a href="{{ url_for('catalog') }}" class="btn btn-outline-primary">Показать все</a>
|
|
93
|
+
{% else %}
|
|
94
|
+
<p>Каталог пока пуст</p>
|
|
95
|
+
{% endif %}
|
|
96
|
+
</div>
|
|
97
|
+
{% endif %}
|
|
98
|
+
{% endblock %}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Оформление заказа{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_head %}
|
|
6
|
+
<script src="https://cdn.jsdelivr.net/npm/inputmask@5/dist/inputmask.min.js"></script>
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
<div class="container py-4">
|
|
11
|
+
<h3 class="mb-4">🛒 Оформление заказа</h3>
|
|
12
|
+
|
|
13
|
+
<!-- Состав заказа -->
|
|
14
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
15
|
+
<div class="card-header bg-dark text-white">
|
|
16
|
+
<h5 class="mb-0">Ваш заказ</h5>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card-body p-0">
|
|
19
|
+
<div class="table-responsive">
|
|
20
|
+
<table class="table table-hover align-middle mb-0">
|
|
21
|
+
<thead class="table-light">
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Товар</th>
|
|
24
|
+
<th>Цена</th>
|
|
25
|
+
<th>Кол-во</th>
|
|
26
|
+
<th>Сумма</th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
{% for item in cart_items %}
|
|
31
|
+
<tr>
|
|
32
|
+
<td>{{ item.product.name }}</td>
|
|
33
|
+
<td>{{ item.product.price|round(2) }} ₽</td>
|
|
34
|
+
<td>{{ item.quantity }}</td>
|
|
35
|
+
<td><strong>{{ (item.product.price * item.quantity)|round(2) }} ₽</strong></td>
|
|
36
|
+
</tr>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</tbody>
|
|
39
|
+
<tfoot>
|
|
40
|
+
<tr class="table-light">
|
|
41
|
+
<td colspan="3" class="text-end"><strong>Итого:</strong></td>
|
|
42
|
+
<td><strong class="text-success fs-5">{{ total_price|round(2) }} ₽</strong></td>
|
|
43
|
+
</tr>
|
|
44
|
+
</tfoot>
|
|
45
|
+
</table>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Форма оформления заказа -->
|
|
51
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
52
|
+
<div class="card-header bg-success text-white">
|
|
53
|
+
<h5 class="mb-0">Данные для оформления</h5>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="card-body">
|
|
56
|
+
<form method="POST" action="{{ url_for('checkout') }}">
|
|
57
|
+
{{CHECKOUT_FIELDS_HTML}}
|
|
58
|
+
<div class="d-flex gap-2 mt-4">
|
|
59
|
+
<button type="submit" class="btn btn-success btn-lg flex-grow-1">
|
|
60
|
+
✅ Оформить заказ
|
|
61
|
+
</button>
|
|
62
|
+
<a href="{{ url_for('cart') }}" class="btn btn-outline-secondary btn-lg">Назад</a>
|
|
63
|
+
</div>
|
|
64
|
+
</form>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{{CHECKOUT_PHONE_MASK}}
|
|
70
|
+
{% endblock %}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Личный кабинет{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_head %}
|
|
6
|
+
<style>
|
|
7
|
+
/* ═══ ПОЛНЫЙ ФИКС дёрганья модального окна ═══ */
|
|
8
|
+
html {
|
|
9
|
+
overflow-y: scroll !important;
|
|
10
|
+
scrollbar-gutter: stable;
|
|
11
|
+
}
|
|
12
|
+
body.modal-open {
|
|
13
|
+
padding-right: 0 !important;
|
|
14
|
+
overflow: hidden !important;
|
|
15
|
+
}
|
|
16
|
+
body.modal-open .navbar,
|
|
17
|
+
body.modal-open .fixed-top {
|
|
18
|
+
padding-right: 0 !important;
|
|
19
|
+
}
|
|
20
|
+
.modal {
|
|
21
|
+
padding-right: 0 !important;
|
|
22
|
+
}
|
|
23
|
+
.modal-backdrop {
|
|
24
|
+
width: 100vw !important;
|
|
25
|
+
}
|
|
26
|
+
/* Убираем ВСЕ анимации/переходы внутри модального окна */
|
|
27
|
+
.modal,
|
|
28
|
+
.modal * {
|
|
29
|
+
transition: none !important;
|
|
30
|
+
animation: none !important;
|
|
31
|
+
will-change: auto !important;
|
|
32
|
+
}
|
|
33
|
+
.modal *:hover,
|
|
34
|
+
.modal *:focus,
|
|
35
|
+
.modal *:active {
|
|
36
|
+
transform: none !important;
|
|
37
|
+
box-shadow: inherit !important;
|
|
38
|
+
transition: none !important;
|
|
39
|
+
}
|
|
40
|
+
.modal.fade,
|
|
41
|
+
.modal.fade .modal-dialog {
|
|
42
|
+
transform: none !important;
|
|
43
|
+
transition: none !important;
|
|
44
|
+
}
|
|
45
|
+
.modal-dialog {
|
|
46
|
+
margin: 80px auto !important;
|
|
47
|
+
transform: none !important;
|
|
48
|
+
transition: none !important;
|
|
49
|
+
will-change: auto !important;
|
|
50
|
+
}
|
|
51
|
+
.modal-content {
|
|
52
|
+
transform: none !important;
|
|
53
|
+
transition: none !important;
|
|
54
|
+
animation: none !important;
|
|
55
|
+
will-change: auto !important;
|
|
56
|
+
}
|
|
57
|
+
/* Отключаем hover-эффекты на элементах ПОД модальным окном */
|
|
58
|
+
body.modal-open .card,
|
|
59
|
+
body.modal-open .card:hover,
|
|
60
|
+
body.modal-open .btn:hover,
|
|
61
|
+
body.modal-open .product-card:hover,
|
|
62
|
+
body.modal-open .table-hover tbody tr:hover {
|
|
63
|
+
transform: none !important;
|
|
64
|
+
box-shadow: inherit !important;
|
|
65
|
+
transition: none !important;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
|
|
69
|
+
{% endblock %}
|
|
70
|
+
|
|
71
|
+
{% block content %}
|
|
72
|
+
<div class="row g-4">
|
|
73
|
+
<!-- Приветствие -->
|
|
74
|
+
<div class="col-12">
|
|
75
|
+
<div class="card border-0 shadow-sm">
|
|
76
|
+
<div class="card-body d-flex align-items-center gap-3 py-3">
|
|
77
|
+
{% if user.avatar %}
|
|
78
|
+
<img src="{{ url_for('static', filename='uploads/avatars/' ~ user.avatar) }}"
|
|
79
|
+
class="rounded-circle" width="56" height="56" style="object-fit:cover;" alt="Аватар">
|
|
80
|
+
{% else %}
|
|
81
|
+
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
|
82
|
+
style="width:56px;height:56px;font-size:24px;">
|
|
83
|
+
{{ (user.email or user.login or 'U')[0]|upper }}
|
|
84
|
+
</div>
|
|
85
|
+
{% endif %}
|
|
86
|
+
<div>
|
|
87
|
+
<h5 class="mb-0">Добро пожаловать, <strong>{{ user.email or user.login or 'Пользователь' }}</strong>!</h5>
|
|
88
|
+
{% if user.full_name is defined and user.full_name %}
|
|
89
|
+
<small class="text-muted">{{ user.full_name }}</small>
|
|
90
|
+
{% endif %}
|
|
91
|
+
</div>
|
|
92
|
+
<div class="ms-auto d-flex gap-2 flex-wrap">
|
|
93
|
+
{{DASHBOARD_ACTION_BUTTONS}}
|
|
94
|
+
<a href="{{ url_for('profile') }}" class="btn btn-outline-secondary btn-sm">
|
|
95
|
+
⚙️ Профиль
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{{DASHBOARD_MAIN_CONTENT}}
|
|
103
|
+
</div>
|
|
104
|
+
{% endblock %}
|
|
105
|
+
|
|
106
|
+
{% block extra_scripts %}
|
|
107
|
+
<script>
|
|
108
|
+
// Анимация удаления строки
|
|
109
|
+
document.querySelectorAll('.delete-btn').forEach(function(btn) {
|
|
110
|
+
btn.closest('form').addEventListener('submit', function(e) {
|
|
111
|
+
const rowId = btn.getAttribute('data-row');
|
|
112
|
+
const row = document.getElementById(rowId);
|
|
113
|
+
if (row) {
|
|
114
|
+
row.style.transition = 'opacity 0.4s, transform 0.4s';
|
|
115
|
+
row.style.opacity = '0';
|
|
116
|
+
row.style.transform = 'translateX(30px)';
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
{% endblock %}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Главная{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block extra_head %}
|
|
6
|
+
{{ANIMATIONS_CSS}}
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
<!-- Слайдер -->
|
|
11
|
+
<div class="slider-wrapper mb-5">
|
|
12
|
+
<div class="slider-container">
|
|
13
|
+
<div class="slides">
|
|
14
|
+
{% for i in range(1, 6) %}
|
|
15
|
+
<div class="slide">
|
|
16
|
+
<img src="{{ url_for('static', filename='images/slide' ~ i ~ '.jpg') }}"
|
|
17
|
+
alt="Слайд {{ i }}" loading="lazy">
|
|
18
|
+
</div>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</div>
|
|
21
|
+
<button class="prev" aria-label="Предыдущий слайд">❮</button>
|
|
22
|
+
<button class="next" aria-label="Следующий слайд">❯</button>
|
|
23
|
+
<!-- Индикаторы -->
|
|
24
|
+
<div class="slider-dots">
|
|
25
|
+
{% for i in range(5) %}
|
|
26
|
+
<span class="dot {% if i == 0 %}active{% endif %}" data-index="{{ i }}"></span>
|
|
27
|
+
{% endfor %}
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Приветствие -->
|
|
33
|
+
<div class="text-center mb-5 {{ANIMATE_CLASS}}">
|
|
34
|
+
<h1 class="display-5 fw-bold">Добро пожаловать!</h1>
|
|
35
|
+
<p class="lead text-muted">{{INDEX_SUBTITLE}}</p>
|
|
36
|
+
{% if not session.user_id %}
|
|
37
|
+
<a href="{{ url_for('register') }}" class="btn btn-primary btn-lg me-2 {{BTN_ANIMATE_CLASS}}">
|
|
38
|
+
Зарегистрироваться
|
|
39
|
+
</a>
|
|
40
|
+
<a href="{{ url_for('login') }}" class="btn btn-outline-secondary btn-lg {{BTN_ANIMATE_CLASS}}">
|
|
41
|
+
Войти
|
|
42
|
+
</a>
|
|
43
|
+
{% else %}
|
|
44
|
+
<a href="{{ url_for('dashboard') }}" class="btn btn-success btn-lg {{BTN_ANIMATE_CLASS}}">
|
|
45
|
+
Перейти в личный кабинет
|
|
46
|
+
</a>
|
|
47
|
+
{% endif %}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{{INDEX_COUNTER_BLOCK}}
|
|
51
|
+
|
|
52
|
+
{{BEFORE_AFTER_SECTION}}
|
|
53
|
+
|
|
54
|
+
<!-- Преимущества -->
|
|
55
|
+
<div class="row g-4 mb-5">
|
|
56
|
+
<div class="col-md-4 {{ANIMATE_CLASS}}">
|
|
57
|
+
<div class="card h-100 text-center border-0 shadow-sm">
|
|
58
|
+
<div class="card-body">
|
|
59
|
+
<div class="fs-1 mb-3">⚡</div>
|
|
60
|
+
<h5 class="card-title">Быстро</h5>
|
|
61
|
+
<p class="card-text text-muted">Обрабатываем заявки в кратчайшие сроки</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="col-md-4 {{ANIMATE_CLASS}}">
|
|
66
|
+
<div class="card h-100 text-center border-0 shadow-sm">
|
|
67
|
+
<div class="card-body">
|
|
68
|
+
<div class="fs-1 mb-3">🛡️</div>
|
|
69
|
+
<h5 class="card-title">Надёжно</h5>
|
|
70
|
+
<p class="card-text text-muted">Гарантируем качество и безопасность</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="col-md-4 {{ANIMATE_CLASS}}">
|
|
75
|
+
<div class="card h-100 text-center border-0 shadow-sm">
|
|
76
|
+
<div class="card-body">
|
|
77
|
+
<div class="fs-1 mb-3">💬</div>
|
|
78
|
+
<h5 class="card-title">Поддержка</h5>
|
|
79
|
+
<p class="card-text text-muted">Всегда на связи и готовы помочь</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{% endblock %}
|
|
85
|
+
|
|
86
|
+
{% block extra_scripts %}
|
|
87
|
+
<script>
|
|
88
|
+
// Обновление счётчика через AJAX каждые 5 секунд
|
|
89
|
+
{{COUNTER_AJAX_SCRIPT}}
|
|
90
|
+
</script>
|
|
91
|
+
{% endblock %}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Вход в систему{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="row justify-content-center">
|
|
7
|
+
<div class="col-md-6 col-lg-4">
|
|
8
|
+
<div class="card shadow">
|
|
9
|
+
<div class="card-header bg-primary text-white">
|
|
10
|
+
<h4 class="mb-0">🔐 Авторизация</h4>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-body p-4">
|
|
13
|
+
<form method="POST" id="loginForm" novalidate>
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="{{LOGIN_FIELD_ID}}" class="form-label fw-semibold">{{LOGIN_FIELD_LABEL}}</label>
|
|
16
|
+
<input type="{{LOGIN_FIELD_TYPE}}" class="form-control" id="{{LOGIN_FIELD_ID}}" name="{{LOGIN_FIELD_ID}}"
|
|
17
|
+
placeholder="{{LOGIN_FIELD_PLACEHOLDER}}" required autocomplete="{{LOGIN_FIELD_AUTOCOMPLETE}}">
|
|
18
|
+
</div>
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<label for="password" class="form-label fw-semibold">Пароль</label>
|
|
21
|
+
<div class="input-group">
|
|
22
|
+
<input type="password" class="form-control" id="password" name="password"
|
|
23
|
+
placeholder="Введите пароль" required autocomplete="current-password">
|
|
24
|
+
<button class="btn btn-outline-secondary" type="button" id="togglePassword"
|
|
25
|
+
aria-label="Показать/скрыть пароль">
|
|
26
|
+
<span id="eyeIcon">👁</span>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<button type="submit" class="btn btn-primary w-100 py-2">Войти</button>
|
|
31
|
+
</form>
|
|
32
|
+
<hr>
|
|
33
|
+
<div class="text-center">
|
|
34
|
+
<a href="{{ url_for('register') }}" class="text-decoration-none">
|
|
35
|
+
Ещё нет аккаунта? <strong>Зарегистрироваться</strong>
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
{% endblock %}
|
|
43
|
+
|
|
44
|
+
{% block extra_scripts %}
|
|
45
|
+
<script>
|
|
46
|
+
// Показать/скрыть пароль
|
|
47
|
+
document.getElementById('togglePassword').addEventListener('click', function() {
|
|
48
|
+
const pw = document.getElementById('password');
|
|
49
|
+
const icon = document.getElementById('eyeIcon');
|
|
50
|
+
if (pw.type === 'password') {
|
|
51
|
+
pw.type = 'text';
|
|
52
|
+
icon.textContent = '🙈';
|
|
53
|
+
} else {
|
|
54
|
+
pw.type = 'password';
|
|
55
|
+
icon.textContent = '👁';
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
{% endblock %}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
db = SQLAlchemy()
|
|
5
|
+
|
|
6
|
+
# ─────────────────────────────────────────────
|
|
7
|
+
# ПОЛЬЗОВАТЕЛЬ
|
|
8
|
+
# ─────────────────────────────────────────────
|
|
9
|
+
class User(db.Model):
|
|
10
|
+
__tablename__ = 'users'
|
|
11
|
+
|
|
12
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
13
|
+
login = db.Column(db.String(80), unique=True, nullable=False)
|
|
14
|
+
password = db.Column(db.String(256), nullable=False)
|
|
15
|
+
role = db.Column(db.String(20), default='user')
|
|
16
|
+
avatar = db.Column(db.String(256), nullable=True)
|
|
17
|
+
is_blocked = db.Column(db.Boolean, default=False)
|
|
18
|
+
login_attempts = db.Column(db.Integer, default=0)
|
|
19
|
+
locked_until = db.Column(db.DateTime, nullable=True)
|
|
20
|
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
21
|
+
|
|
22
|
+
{{USER_EXTRA_FIELDS}}
|
|
23
|
+
|
|
24
|
+
# Связи
|
|
25
|
+
requests = db.relationship('Request', backref='user', lazy=True, cascade='all, delete-orphan')
|
|
26
|
+
{{CART_USER_RELATION}}
|
|
27
|
+
{{ORDER_USER_RELATION}}
|
|
28
|
+
{{REVIEW_USER_RELATION}}
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return f'<User {self.login}>'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────
|
|
35
|
+
# ЗАЯВКА
|
|
36
|
+
# ─────────────────────────────────────────────
|
|
37
|
+
class Request(db.Model):
|
|
38
|
+
__tablename__ = 'requests'
|
|
39
|
+
|
|
40
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
41
|
+
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
42
|
+
status = db.Column(db.String(50), default='Новая')
|
|
43
|
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
44
|
+
|
|
45
|
+
{{REQUEST_EXTRA_FIELDS}}
|
|
46
|
+
{{PHOTO_FIELDS}}
|
|
47
|
+
{{SERVICE_FK}}
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return f'<Request {self.id} [{self.status}]>'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
{{SERVICE_MODEL}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
{{PRODUCT_MODEL}}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
{{CART_MODELS}}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
{{ORDER_MODELS}}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
{{REVIEW_MODEL}}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Новая заявка{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="row justify-content-center">
|
|
7
|
+
<div class="col-md-8 col-lg-6">
|
|
8
|
+
<div class="card shadow">
|
|
9
|
+
<div class="card-header bg-success text-white">
|
|
10
|
+
<h4 class="mb-0">📋 Создание заявки</h4>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-body p-4">
|
|
13
|
+
<form method="POST" enctype="multipart/form-data" id="requestForm" novalidate>
|
|
14
|
+
{{REQUEST_FIELDS_HTML}}
|
|
15
|
+
{{PHOTO_BEFORE_FIELD}}
|
|
16
|
+
<div class="d-flex gap-2 mt-4">
|
|
17
|
+
<button type="submit" class="btn btn-success flex-grow-1 py-2">
|
|
18
|
+
✅ Отправить заявку
|
|
19
|
+
</button>
|
|
20
|
+
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">
|
|
21
|
+
Отмена
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
</form>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
{% endblock %}
|
|
30
|
+
|
|
31
|
+
{% block extra_scripts %}
|
|
32
|
+
<script>
|
|
33
|
+
// Предпросмотр фото "до"
|
|
34
|
+
const photoBeforeInput = document.getElementById('photo_before');
|
|
35
|
+
if (photoBeforeInput) {
|
|
36
|
+
photoBeforeInput.addEventListener('change', function() {
|
|
37
|
+
const preview = document.getElementById('photoBeforePreview');
|
|
38
|
+
if (this.files && this.files[0]) {
|
|
39
|
+
const reader = new FileReader();
|
|
40
|
+
reader.onload = function(e) {
|
|
41
|
+
preview.src = e.target.result;
|
|
42
|
+
preview.classList.remove('d-none');
|
|
43
|
+
};
|
|
44
|
+
reader.readAsDataURL(this.files[0]);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
{% endblock %}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Мои заказы{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<h3 class="mb-4">📦 История заказов</h3>
|
|
7
|
+
|
|
8
|
+
{% if orders %}
|
|
9
|
+
<div class="row g-3">
|
|
10
|
+
{% for order in orders %}
|
|
11
|
+
<div class="col-12">
|
|
12
|
+
<div class="card border-0 shadow-sm">
|
|
13
|
+
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
14
|
+
<div>
|
|
15
|
+
<strong>Заказ #{{ order.id }}</strong>
|
|
16
|
+
<span class="text-muted ms-2 small">{{ order.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
{% set order_colors = {'Новый': 'warning', 'Оплачен': 'info', 'Отправлен': 'primary', 'Доставлен': 'success', 'Отменён': 'danger'} %}
|
|
19
|
+
<span class="badge bg-{{ order_colors.get(order.status, 'secondary') }}
|
|
20
|
+
{% if order.status == 'Новый' %}text-dark{% endif %}">
|
|
21
|
+
{{ order.status }}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="card-body">
|
|
25
|
+
<div class="table-responsive">
|
|
26
|
+
<table class="table table-sm mb-0">
|
|
27
|
+
<thead class="table-light">
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Товар</th>
|
|
30
|
+
<th>Кол-во</th>
|
|
31
|
+
<th>Цена</th>
|
|
32
|
+
<th>Сумма</th>
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody>
|
|
36
|
+
{% for item in order.items %}
|
|
37
|
+
<tr>
|
|
38
|
+
<td>{{ item.product_name }}</td>
|
|
39
|
+
<td>{{ item.quantity }}</td>
|
|
40
|
+
<td>{{ item.price }} ₽</td>
|
|
41
|
+
<td><strong>{{ item.price * item.quantity }} ₽</strong></td>
|
|
42
|
+
</tr>
|
|
43
|
+
{% endfor %}
|
|
44
|
+
</tbody>
|
|
45
|
+
<tfoot>
|
|
46
|
+
<tr class="table-light">
|
|
47
|
+
<td colspan="3" class="text-end fw-bold">Итого:</td>
|
|
48
|
+
<td class="fw-bold text-success">{{ order.total_price }} ₽</td>
|
|
49
|
+
</tr>
|
|
50
|
+
</tfoot>
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
{% endfor %}
|
|
57
|
+
</div>
|
|
58
|
+
{% else %}
|
|
59
|
+
<div class="text-center py-5 text-muted">
|
|
60
|
+
<div class="fs-1">📦</div>
|
|
61
|
+
<h5 class="mt-3">Заказов пока нет</h5>
|
|
62
|
+
<a href="{{ url_for('catalog') }}" class="btn btn-primary">Перейти в каталог</a>
|
|
63
|
+
</div>
|
|
64
|
+
{% endif %}
|
|
65
|
+
{% endblock %}
|