@mashka818/exam-de-template 1.0.0 → 1.0.2
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.
- package/exam-guides/EXAM_COMMANDS.txt +1 -1
- package/exam-guides/commented-code/client/index.html +7 -0
- package/exam-guides/commented-code/client/package-lock.json +541 -6
- package/exam-guides/commented-code/client/package.json +1 -0
- package/exam-guides/commented-code/client/public/images/README.txt +4 -24
- package/exam-guides/commented-code/client/public/images/about-cleaning.png +0 -0
- package/exam-guides/commented-code/client/public/images/admin-banner.png +0 -0
- package/exam-guides/commented-code/client/public/images/empty-requests.png +0 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-1.png +0 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-2.png +0 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-3.png +0 -0
- package/exam-guides/commented-code/client/public/images/home-hero.png +0 -0
- package/exam-guides/commented-code/client/public/images/login-banner.png +0 -0
- package/exam-guides/commented-code/client/public/images/logo.png +0 -0
- package/exam-guides/commented-code/client/public/images/new-request-banner.png +0 -0
- package/exam-guides/commented-code/client/public/images/register-banner.png +0 -0
- package/exam-guides/commented-code/client/public/images/requests-banner.png +0 -0
- package/exam-guides/commented-code/client/public/images/slide-1.png +0 -0
- package/exam-guides/commented-code/client/public/images/slide-2.png +0 -0
- package/exam-guides/commented-code/client/public/images/slide-3.png +0 -0
- package/exam-guides/commented-code/client/src/App.jsx +14 -0
- package/exam-guides/commented-code/client/src/api.js +16 -2
- package/exam-guides/commented-code/client/src/components/FormField.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/Layout.jsx +8 -0
- package/exam-guides/commented-code/client/src/components/PageImage.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/UserNav.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +7 -0
- package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +7 -0
- package/exam-guides/commented-code/client/src/config/images.js +26 -28
- package/exam-guides/commented-code/client/src/constants/services.js +7 -0
- package/exam-guides/commented-code/client/src/context/AuthContext.jsx +8 -0
- package/exam-guides/commented-code/client/src/index.css +8 -0
- package/exam-guides/commented-code/client/src/main.jsx +8 -0
- package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +59 -24
- package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +7 -0
- package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +8 -0
- package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +8 -0
- package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +7 -0
- package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +7 -0
- package/exam-guides/commented-code/client/src/utils/validation.js +8 -0
- package/exam-guides/commented-code/client/vite.config.js +7 -0
- package/exam-guides/commented-code/server/.env.example +8 -0
- package/exam-guides/commented-code/server/db/init.js +7 -0
- package/exam-guides/commented-code/server/db/pool.js +7 -0
- package/exam-guides/commented-code/server/db/schema.sql +5 -0
- package/exam-guides/commented-code/server/db/seed.sql +5 -0
- package/exam-guides/commented-code/server/index.js +7 -0
- package/exam-guides/commented-code/server/middleware/auth.js +7 -0
- package/exam-guides/commented-code/server/routes/admin.js +23 -3
- package/exam-guides/commented-code/server/routes/auth.js +7 -0
- package/exam-guides/commented-code/server/routes/requests.js +7 -0
- package/exam-guides/commented-code/server/routes/services.js +7 -0
- package/exam-guides/commented-code/server/utils/validation.js +7 -0
- package/exam-project/EXAM_COMMANDS.txt +47 -0
- package/exam-project/README.md +4 -7
- package/exam-project/THEME_BANQUETAM_NET.md +106 -0
- package/exam-project/client/index.html +7 -0
- package/exam-project/client/package-lock.json +541 -6
- package/exam-project/client/package.json +1 -0
- package/exam-project/client/public/images/README.txt +4 -24
- package/exam-project/client/public/images/about-cleaning.png +0 -0
- package/exam-project/client/public/images/admin-banner.png +0 -0
- package/exam-project/client/public/images/empty-requests.png +0 -0
- package/exam-project/client/public/images/footer-photo-1.png +0 -0
- package/exam-project/client/public/images/footer-photo-2.png +0 -0
- package/exam-project/client/public/images/footer-photo-3.png +0 -0
- package/exam-project/client/public/images/home-hero.png +0 -0
- package/exam-project/client/public/images/login-banner.png +0 -0
- package/exam-project/client/public/images/logo.png +0 -0
- package/exam-project/client/public/images/new-request-banner.png +0 -0
- package/exam-project/client/public/images/register-banner.png +0 -0
- package/exam-project/client/public/images/requests-banner.png +0 -0
- package/exam-project/client/public/images/slide-1.png +0 -0
- package/exam-project/client/public/images/slide-2.png +0 -0
- package/exam-project/client/public/images/slide-3.png +0 -0
- package/exam-project/client/src/App.jsx +35 -1
- package/exam-project/client/src/api.js +37 -2
- package/exam-project/client/src/components/FormField.jsx +22 -1
- package/exam-project/client/src/components/Layout.jsx +34 -4
- package/exam-project/client/src/components/PageImage.jsx +24 -1
- package/exam-project/client/src/components/ProtectedRoute.jsx +23 -1
- package/exam-project/client/src/components/UserNav.jsx +20 -0
- package/exam-project/client/src/components/landing/HeroSlider.jsx +23 -2
- package/exam-project/client/src/components/landing/LandingLayout.jsx +23 -1
- package/exam-project/client/src/components/landing/SiteFooter.jsx +20 -0
- package/exam-project/client/src/config/images.js +45 -26
- package/exam-project/client/src/constants/services.js +20 -1
- package/exam-project/client/src/context/AuthContext.jsx +26 -1
- package/exam-project/client/src/index.css +28 -1
- package/exam-project/client/src/main.jsx +20 -1
- package/exam-project/client/src/pages/AdminPage.jsx +65 -7
- package/exam-project/client/src/pages/LandingPage.jsx +23 -0
- package/exam-project/client/src/pages/LoginPage.jsx +28 -2
- package/exam-project/client/src/pages/RegisterPage.jsx +29 -1
- package/exam-project/client/src/pages/RequestFormPage.jsx +28 -3
- package/exam-project/client/src/pages/RequestsPage.jsx +26 -1
- package/exam-project/client/src/utils/validation.js +26 -1
- package/exam-project/client/vite.config.js +22 -1
- package/exam-project/scripts/unpack-template.js +2 -1
- package/exam-project/server/.env.example +8 -0
- package/exam-project/server/db/init.js +27 -3
- package/exam-project/server/db/pool.js +20 -1
- package/exam-project/server/db/schema.sql +17 -0
- package/exam-project/server/db/seed.sql +8 -0
- package/exam-project/server/index.js +24 -1
- package/exam-project/server/middleware/auth.js +22 -1
- package/exam-project/server/routes/admin.js +44 -4
- package/exam-project/server/routes/auth.js +27 -1
- package/exam-project/server/routes/requests.js +27 -4
- package/exam-project/server/routes/services.js +21 -1
- package/exam-project/server/utils/validation.js +27 -2
- package/package.json +1 -1
- package/scripts/unpack-template.js +2 -1
- package/exam-guides/commented-code/client/public/images/about-cleaning.svg +0 -4
- package/exam-guides/commented-code/client/public/images/admin-banner.svg +0 -1
- package/exam-guides/commented-code/client/public/images/empty-requests.svg +0 -1
- package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +0 -4
- package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +0 -4
- package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +0 -4
- package/exam-guides/commented-code/client/public/images/home-hero.svg +0 -4
- package/exam-guides/commented-code/client/public/images/login-banner.svg +0 -1
- package/exam-guides/commented-code/client/public/images/logo.svg +0 -4
- package/exam-guides/commented-code/client/public/images/new-request-banner.svg +0 -1
- package/exam-guides/commented-code/client/public/images/register-banner.svg +0 -1
- package/exam-guides/commented-code/client/public/images/requests-banner.svg +0 -1
- package/exam-guides/commented-code/client/public/images/slide-1.svg +0 -6
- package/exam-guides/commented-code/client/public/images/slide-2.svg +0 -5
- package/exam-guides/commented-code/client/public/images/slide-3.svg +0 -5
- package/exam-project/client/public/images/about-cleaning.svg +0 -4
- package/exam-project/client/public/images/admin-banner.svg +0 -1
- package/exam-project/client/public/images/empty-requests.svg +0 -1
- package/exam-project/client/public/images/footer-photo-1.svg +0 -4
- package/exam-project/client/public/images/footer-photo-2.svg +0 -4
- package/exam-project/client/public/images/footer-photo-3.svg +0 -4
- package/exam-project/client/public/images/home-hero.svg +0 -4
- package/exam-project/client/public/images/login-banner.svg +0 -1
- package/exam-project/client/public/images/logo.svg +0 -4
- package/exam-project/client/public/images/new-request-banner.svg +0 -1
- package/exam-project/client/public/images/register-banner.svg +0 -1
- package/exam-project/client/public/images/requests-banner.svg +0 -1
- package/exam-project/client/public/images/slide-1.svg +0 -6
- package/exam-project/client/public/images/slide-2.svg +0 -5
- package/exam-project/client/public/images/slide-3.svg +0 -5
|
@@ -1,27 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/AdminPage.jsx | URL: /admin | п.5
|
|
4
|
+
* Фильтр filterStatus → api.adminGetRequests({ status }). Кнопки статусов → PATCH.
|
|
5
|
+
* Сервер: server/routes/admin.js
|
|
6
|
+
* =============================================================================
|
|
7
|
+
*/
|
|
1
8
|
|
|
2
|
-
|
|
9
|
+
/**
|
|
10
|
+
* =============================================================================
|
|
11
|
+
* АДМИН-ПАНЕЛЬ (/admin) — п.5 задания ДЭ
|
|
12
|
+
* =============================================================================
|
|
13
|
+
* ФИЛЬТР ПО СТАТУСУ:
|
|
14
|
+
* filterStatus — значение select (all / new / in_progress / completed / cancelled)
|
|
15
|
+
* load() → api.adminGetRequests({ status }) → GET /api/admin/requests?status=...
|
|
16
|
+
* На сервере: server/routes/admin.js — WHERE r.status = $1
|
|
17
|
+
*
|
|
18
|
+
* ЗАМЕНИТЕ: кнопки статусов, admin-banner, логин админа в server/db/init.js
|
|
19
|
+
* БАНКЕТАМ.НЕТ: подписи опций фильтра и кнопок — «Банкет назначен» и т.д.
|
|
20
|
+
* =============================================================================
|
|
21
|
+
*/
|
|
22
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
23
|
import Layout from '../components/Layout.jsx';
|
|
4
24
|
import PageImage from '../components/PageImage.jsx';
|
|
5
25
|
import { api } from '../api.js';
|
|
6
26
|
|
|
27
|
+
const STATUS_FILTER_OPTIONS = [
|
|
28
|
+
{ value: 'all', label: 'Все заявки' },
|
|
29
|
+
{ value: 'new', label: 'Новая' },
|
|
30
|
+
{ value: 'in_progress', label: 'В работе' },
|
|
31
|
+
{ value: 'completed', label: 'Выполнено' },
|
|
32
|
+
{ value: 'cancelled', label: 'Отменено' },
|
|
33
|
+
];
|
|
34
|
+
|
|
7
35
|
export default function AdminPage() {
|
|
8
36
|
const [requests, setRequests] = useState([]);
|
|
9
37
|
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [filterStatus, setFilterStatus] = useState('all');
|
|
10
39
|
const [cancelReasons, setCancelReasons] = useState({});
|
|
11
40
|
const [message, setMessage] = useState('');
|
|
12
41
|
|
|
13
|
-
const load = () => {
|
|
42
|
+
const load = useCallback(() => {
|
|
14
43
|
setLoading(true);
|
|
15
44
|
api
|
|
16
|
-
.adminGetRequests()
|
|
45
|
+
.adminGetRequests({ status: filterStatus })
|
|
17
46
|
.then(setRequests)
|
|
18
47
|
.catch((e) => setMessage(e.message))
|
|
19
48
|
.finally(() => setLoading(false));
|
|
20
|
-
};
|
|
49
|
+
}, [filterStatus]);
|
|
21
50
|
|
|
22
51
|
useEffect(() => {
|
|
23
52
|
load();
|
|
24
|
-
}, []);
|
|
53
|
+
}, [load]);
|
|
25
54
|
|
|
26
55
|
const updateStatus = async (id, status) => {
|
|
27
56
|
setMessage('');
|
|
@@ -32,6 +61,7 @@ export default function AdminPage() {
|
|
|
32
61
|
}
|
|
33
62
|
try {
|
|
34
63
|
await api.adminUpdateStatus(id, { status, cancelReason });
|
|
64
|
+
setMessage('Статус обновлён');
|
|
35
65
|
load();
|
|
36
66
|
} catch (e) {
|
|
37
67
|
setMessage(e.data?.message || e.message);
|
|
@@ -43,10 +73,39 @@ export default function AdminPage() {
|
|
|
43
73
|
<div className="animate-fade-up">
|
|
44
74
|
<PageImage imageKey="adminBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
|
|
45
75
|
|
|
46
|
-
|
|
47
76
|
<h2 className="text-xl font-bold text-white mb-4">Панель администратора</h2>
|
|
77
|
+
|
|
78
|
+
{/* Фильтр: меняйте label в STATUS_FILTER_OPTIONS; value = status в БД */}
|
|
79
|
+
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
80
|
+
<label htmlFor="admin-filter-status" className="text-white/90 text-sm">
|
|
81
|
+
Статус:
|
|
82
|
+
</label>
|
|
83
|
+
<select
|
|
84
|
+
id="admin-filter-status"
|
|
85
|
+
value={filterStatus}
|
|
86
|
+
onChange={(e) => setFilterStatus(e.target.value)}
|
|
87
|
+
className="rounded-lg border border-violet-200 px-3 py-1.5 text-sm text-slate-800 bg-white"
|
|
88
|
+
>
|
|
89
|
+
{STATUS_FILTER_OPTIONS.map((opt) => (
|
|
90
|
+
<option key={opt.value} value={opt.value}>
|
|
91
|
+
{opt.label}
|
|
92
|
+
</option>
|
|
93
|
+
))}
|
|
94
|
+
</select>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={load}
|
|
98
|
+
className="text-xs px-2 py-1.5 rounded-lg bg-white/20 text-white hover:bg-white/30"
|
|
99
|
+
>
|
|
100
|
+
Обновить
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
48
104
|
{message && <p className="text-amber-200 text-sm mb-3">{message}</p>}
|
|
49
105
|
{loading && <p className="text-white/70">Загрузка…</p>}
|
|
106
|
+
{!loading && requests.length === 0 && (
|
|
107
|
+
<p className="text-white/70 text-sm">Нет заявок с выбранным статусом</p>
|
|
108
|
+
)}
|
|
50
109
|
|
|
51
110
|
<div className="space-y-4">
|
|
52
111
|
{requests.map((r) => (
|
|
@@ -75,7 +134,6 @@ export default function AdminPage() {
|
|
|
75
134
|
{new Date(r.scheduledAt).toLocaleString('ru-RU')} · {r.paymentLabel}
|
|
76
135
|
</p>
|
|
77
136
|
|
|
78
|
-
|
|
79
137
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
80
138
|
{r.status === 'new' && (
|
|
81
139
|
<button
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/LandingPage.jsx | URL: /
|
|
4
|
+
* Лендинг: слайдер, тексты perks, блок услуг. Не путать с Layout.jsx
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
1
7
|
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* ЛЕНДИНГ (/) — главная для гостей
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* Слайдер, о клининге, услуги, футер с фото-заглушками.
|
|
13
|
+
* Картинки: config/images.js → public/images/
|
|
14
|
+
* БАНКЕТАМ.НЕТ: тексты про банкеты; perks и «Наши услуги» = 4 помещения;
|
|
15
|
+
* LandingLayout brandName → Банкетам.Нет; слайдер 4×3 сек (HeroSlider).
|
|
16
|
+
*
|
|
17
|
+
* СТРУКТУРА: LandingLayout → HeroSlider → секции → SiteFooter
|
|
18
|
+
* perks[] — карточки преимуществ (массив строк, менять под тему)
|
|
19
|
+
* DEFAULT_SERVICE_NAMES — подписи на лендинге (дубль seed.sql; синхронизировать)
|
|
20
|
+
* cabinetPath — куда ведёт CTA для вошедшего пользователя
|
|
21
|
+
*
|
|
22
|
+
* GUIDE_PAGES.md §5 | config/images.js (aboutCleaning, homeHero…)
|
|
23
|
+
* =============================================================================
|
|
24
|
+
*/
|
|
2
25
|
import { Link } from 'react-router-dom';
|
|
3
26
|
import { useAuth } from '../context/AuthContext.jsx';
|
|
4
27
|
import LandingLayout from '../components/landing/LandingLayout.jsx';
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/LoginPage.jsx | URL: /login | п.2
|
|
4
|
+
* login + password → AuthContext.login → admin: /admin, user: /requests
|
|
5
|
+
* Админ: server/db/init.js (adminka или Admin26)
|
|
6
|
+
* =============================================================================
|
|
7
|
+
*/
|
|
1
8
|
|
|
9
|
+
/**
|
|
10
|
+
* =============================================================================
|
|
11
|
+
* АВТОРИЗАЦИЯ (/login) — п.2 задания ДЭ
|
|
12
|
+
* =============================================================================
|
|
13
|
+
* ЗАМЕНИТЕ:
|
|
14
|
+
* сообщения об ошибке — уже есть «Неверный логин или пароль»
|
|
15
|
+
* adminka/password — заданы в server/db/init.js (не на этой странице)
|
|
16
|
+
* картинку — login-banner в public/images/
|
|
17
|
+
* Отличительная черта: тёмная шапка (variant="login")
|
|
18
|
+
* БАНКЕТАМ.НЕТ (п.2): ссылка «Еще не зарегистрированы? Регистрация» (сейчас «Создать аккаунт»).
|
|
19
|
+
* БАНКЕТАМ.НЕТ: подсказка админа Admin26 / Demo20 внизу формы.
|
|
20
|
+
*
|
|
21
|
+
* ПОТОК: login({ login, password }) → JWT в localStorage
|
|
22
|
+
* user.role === 'admin' → /admin, иначе → /requests
|
|
23
|
+
* Админ создаётся в server/db/init.js (не на этой странице)
|
|
24
|
+
*
|
|
25
|
+
* GUIDE_PAGES.md §2.2 | server/db/init.js (adminka / Admin26)
|
|
26
|
+
* =============================================================================
|
|
27
|
+
*/
|
|
2
28
|
import { useState } from 'react';
|
|
3
29
|
import { Link, useNavigate } from 'react-router-dom';
|
|
4
30
|
import Layout from '../components/Layout.jsx';
|
|
@@ -49,11 +75,11 @@ export default function LoginPage() {
|
|
|
49
75
|
Войти
|
|
50
76
|
</button>
|
|
51
77
|
</form>
|
|
52
|
-
|
|
78
|
+
{/* БАНКЕТАМ.НЕТ: текст ссылки → «Еще не зарегистрированы? Регистрация» */}
|
|
53
79
|
<p className="text-center text-sm mt-4">
|
|
54
80
|
<Link to="/register" className="text-teal-700 underline">Создать аккаунт</Link>
|
|
55
81
|
</p>
|
|
56
|
-
|
|
82
|
+
{/* БАНКЕТАМ.НЕТ: Admin26 / Demo20 */}
|
|
57
83
|
<p className="text-xs text-slate-500 mt-6 text-center">
|
|
58
84
|
Админ: логин <code>adminka</code>, пароль <code>password</code>
|
|
59
85
|
</p>
|
|
@@ -1,4 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/RegisterPage.jsx | URL: /register | п.1
|
|
4
|
+
* Поля: login, password, fullName, phone, email → POST /api/auth/register
|
|
5
|
+
* Менять: label, banner imageKey registerBanner, validation.js
|
|
6
|
+
* =============================================================================
|
|
7
|
+
*/
|
|
1
8
|
|
|
9
|
+
/**
|
|
10
|
+
* =============================================================================
|
|
11
|
+
* РЕГИСТРАЦИЯ (/register) — п.1 задания ДЭ
|
|
12
|
+
* =============================================================================
|
|
13
|
+
* ЗАМЕНИТЕ:
|
|
14
|
+
* label полей — если в теме другие данные пользователя (не ФИО, а «Название фирмы»)
|
|
15
|
+
* валидацию — utils/validation.js (сервер + клиент)
|
|
16
|
+
* API-тело — те же поля уходят в POST /api/auth/register
|
|
17
|
+
* картинку — register-banner в public/images/
|
|
18
|
+
* Отличительная черта: бирюзовая шапка (variant="register")
|
|
19
|
+
* БАНКЕТАМ.НЕТ (п.1): логин латиница+цифры ≥6, пароль ≥8; ссылка «Уже есть аккаунт? Вход».
|
|
20
|
+
* БАНКЕТАМ.НЕТ: brand в Layout → «Банкетам.Нет».
|
|
21
|
+
*
|
|
22
|
+
* ПОТОК: form state → validateRegistration → register() из AuthContext
|
|
23
|
+
* → POST /api/auth/register → navigate('/requests')
|
|
24
|
+
* Ошибки: errors.xxx под полями; serverError — общая строка сверху
|
|
25
|
+
* inputClass — стиль полей; при ошибке добавьте className input-error
|
|
26
|
+
*
|
|
27
|
+
* GUIDE_PAGES.md §2.1 | server/routes/auth.js
|
|
28
|
+
* =============================================================================
|
|
29
|
+
*/
|
|
2
30
|
import { useState } from 'react';
|
|
3
31
|
import { Link, useNavigate } from 'react-router-dom';
|
|
4
32
|
import Layout from '../components/Layout.jsx';
|
|
@@ -87,7 +115,7 @@ export default function RegisterPage() {
|
|
|
87
115
|
</button>
|
|
88
116
|
</form>
|
|
89
117
|
|
|
90
|
-
|
|
118
|
+
{/* БАНКЕТАМ.НЕТ: на странице регистрации — ссылка «Уже зарегистрированы? Вход» (сейчас «Уже есть аккаунт?») */}
|
|
91
119
|
<p className="text-center text-sm mt-4 text-slate-600">
|
|
92
120
|
Уже есть аккаунт? <Link to="/login" className="text-teal-700 underline">Войти</Link>
|
|
93
121
|
</p>
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/RequestFormPage.jsx | URL: /requests/form | п.4
|
|
4
|
+
* Форма заявки: api.getServices() + api.createRequest(). Поля form state → validation → POST
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* п.4 ЗАДАНИЯ — «Страница формирования заявки» (/requests/form)
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* Поля: адрес, контактные данные (телефон), дата/время, вид услуги, оплата.
|
|
13
|
+
* «Иная услуга» — чекбокс + текст (модуль ПУ).
|
|
14
|
+
* После отправки — редирект на п.3 (/requests).
|
|
15
|
+
* БАНКЕТАМ.НЕТ (п.4): select помещение; дата type="text" placeholder ДД.ММ.ГГГГ (не datetime-local).
|
|
16
|
+
* БАНКЕТАМ.НЕТ: убрать блок «Иная услуга» (isCustomService).
|
|
17
|
+
* БАНКЕТАМ.НЕТ: заголовок «Оформление бронирования», label «Помещение».
|
|
18
|
+
*
|
|
19
|
+
* services: api.getServices() → select option (id, name) из service_types
|
|
20
|
+
* submit: validateRequestForm → api.createRequest(form) → navigate('/requests')
|
|
21
|
+
* paymentType: 'cash' | 'card' — значения как в schema CHECK
|
|
22
|
+
* isCustomService — модуль ПУ; для Банкетам.Нет удалить блок и поля в API
|
|
23
|
+
*
|
|
24
|
+
* GUIDE_PAGES.md §2.4 | server/routes/requests.js POST /
|
|
25
|
+
* =============================================================================
|
|
26
|
+
*/
|
|
2
27
|
import { useEffect, useState } from 'react';
|
|
3
28
|
import { Link, useNavigate } from 'react-router-dom';
|
|
4
29
|
import Layout from '../components/Layout.jsx';
|
|
@@ -79,7 +104,7 @@ export default function RequestFormPage() {
|
|
|
79
104
|
className="bg-white rounded-2xl shadow-lg p-6 animate-fade-up border border-cyan-100"
|
|
80
105
|
noValidate
|
|
81
106
|
>
|
|
82
|
-
|
|
107
|
+
{/* БАНКЕТАМ.НЕТ: «Оформление бронирования» */}
|
|
83
108
|
<h1 className="text-xl font-bold text-cyan-900 mb-1">Формирование заявки</h1>
|
|
84
109
|
<p className="text-sm text-slate-500 mb-6">Все поля обязательны</p>
|
|
85
110
|
|
|
@@ -130,7 +155,7 @@ export default function RequestFormPage() {
|
|
|
130
155
|
</FormField>
|
|
131
156
|
)}
|
|
132
157
|
|
|
133
|
-
|
|
158
|
+
{/* БАНКЕТАМ.НЕТ: удалить весь блок «Иная услуга» — в задании только select помещений */}
|
|
134
159
|
<label className="flex items-center gap-2 mb-4 cursor-pointer">
|
|
135
160
|
<input
|
|
136
161
|
type="checkbox"
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/pages/RequestsPage.jsx | URL: /requests | п.3
|
|
4
|
+
* История: api.getMyRequests(). Кнопка на /requests/form. statusLabel с сервера.
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
1
7
|
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* п.3 ЗАДАНИЯ — «Страница создания заявки» (/requests)
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* История заявок + кнопка перехода на /requests/form (п.4)
|
|
13
|
+
* БАНКЕТАМ.НЕТ (п.3): «Личный кабинет» — история бронирований + слайдер (HeroSlider).
|
|
14
|
+
* БАНКЕТАМ.НЕТ: блок отзыва под заявкой, если status !== 'new' (после админа).
|
|
15
|
+
* БАНКЕТАМ.НЕТ: statusColors — подписи «Банкет назначен» / «Банкет завершен».
|
|
16
|
+
*
|
|
17
|
+
* ДАННЫЕ: useEffect → api.getMyRequests() → GET /api/requests/mine
|
|
18
|
+
* statusLabel приходит с сервера (requests.js STATUS_LABELS)
|
|
19
|
+
* Кнопка «Новая заявка» → Link to="/requests/form" (п.4)
|
|
20
|
+
*
|
|
21
|
+
* БАНКЕТАМ.НЕТ (отзывы): под карточкой textarea + кнопка, если status !== 'new'
|
|
22
|
+
* → api.postReview + route на сервере + таблица reviews в schema.sql
|
|
23
|
+
*
|
|
24
|
+
* GUIDE_PAGES.md §2.3 | THEME_BANQUETAM_NET.md
|
|
25
|
+
* =============================================================================
|
|
26
|
+
*/
|
|
2
27
|
import { useEffect, useState } from 'react';
|
|
3
28
|
import { Link } from 'react-router-dom';
|
|
4
29
|
import Layout from '../components/Layout.jsx';
|
|
@@ -31,7 +56,7 @@ export default function RequestsPage() {
|
|
|
31
56
|
<p className="text-white/70 text-xs mb-1">Пункт 3 задания</p>
|
|
32
57
|
<PageImage imageKey="requestsBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
|
|
33
58
|
|
|
34
|
-
|
|
59
|
+
{/* БАНКЕТАМ.НЕТ: заголовок «Личный кабинет» */}
|
|
35
60
|
<h1 className="text-xl font-bold text-white drop-shadow mb-1">Создание заявки</h1>
|
|
36
61
|
<p className="text-white/80 text-sm mb-6">
|
|
37
62
|
История ваших заявок и переход к оформлению новой
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/src/utils/validation.js
|
|
4
|
+
* Валидация форм на клиенте. ДУБЛИРОВАТЬ в server/utils/validation.js!
|
|
5
|
+
* register → RegisterPage | request → RequestFormPage
|
|
6
|
+
* =============================================================================
|
|
7
|
+
*/
|
|
1
8
|
|
|
9
|
+
/**
|
|
10
|
+
* =============================================================================
|
|
11
|
+
* ВАЛИДАЦИЯ НА КЛИЕНТЕ (мгновенные ошибки на форме)
|
|
12
|
+
* =============================================================================
|
|
13
|
+
* Должна совпадать с server/utils/validation.js
|
|
14
|
+
* ЗАМЕНИТЕ регулярки и сообщения, если в задании другие правила.
|
|
15
|
+
* БАНКЕТАМ.НЕТ: см. server/utils/validation.js (логин, пароль 8, дата ДД.ММ.ГГГГ).
|
|
16
|
+
*
|
|
17
|
+
* ВАЖНО: любое изменение здесь — продублировать в server/utils/validation.js
|
|
18
|
+
* Иначе форма пройдёт на клиенте, но сервер вернёт 400.
|
|
19
|
+
*
|
|
20
|
+
* validateRegistration — RegisterPage submit
|
|
21
|
+
* validateRequestForm — RequestFormPage submit
|
|
22
|
+
* formatPhoneInput — onChange телефона (маска +7(999)-999-99-99)
|
|
23
|
+
*
|
|
24
|
+
* GUIDE_PAGES.md §7.1 | THEME_BANQUETAM_NET.md
|
|
25
|
+
* =============================================================================
|
|
26
|
+
*/
|
|
2
27
|
|
|
3
28
|
/** Телефон строго в формате задания — менять вместе с подсказкой на форме */
|
|
4
29
|
export const PHONE_REGEX = /^\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}$/;
|
|
@@ -10,7 +35,7 @@ export function validateRegistration(form) {
|
|
|
10
35
|
const errors = {};
|
|
11
36
|
if (!form.login?.trim()) errors.login = 'Логин обязателен';
|
|
12
37
|
if (!form.password) errors.password = 'Пароль обязателен';
|
|
13
|
-
else if (form.password.length < 6) errors.password = 'Минимум 6 символов';
|
|
38
|
+
else if (form.password.length < 6) errors.password = 'Минимум 6 символов'; // БАНКЕТАМ.НЕТ: 8
|
|
14
39
|
if (!form.fullName?.trim()) errors.fullName = 'ФИО обязательно';
|
|
15
40
|
else if (!FIO_REGEX.test(form.fullName.trim())) errors.fullName = 'Только кириллица и пробелы';
|
|
16
41
|
if (!form.phone?.trim()) errors.phone = 'Телефон обязателен';
|
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: client/vite.config.js
|
|
4
|
+
* Порт 5173. proxy /api → :3001 (если сменили PORT в server/.env — поменять target)
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import { defineConfig } from 'vite';
|
|
2
9
|
import react from '@vitejs/plugin-react';
|
|
3
10
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
/**
|
|
13
|
+
* =============================================================================
|
|
14
|
+
* vite.config.js — СБОРКА И DEV-СЕРВЕР ФРОНТА
|
|
15
|
+
* =============================================================================
|
|
16
|
+
* port: 5173 — адрес сайта http://localhost:5173
|
|
17
|
+
* proxy /api → http://localhost:3001 — поэтому в api.js пишем fetch('/api/...')
|
|
18
|
+
*
|
|
19
|
+
* Картинки: всё из client/public/ отдаётся как /имя-файла (папка images → /images/...)
|
|
20
|
+
*
|
|
21
|
+
* Если сменили PORT в server/.env (например 3002) — поменяйте target ниже.
|
|
22
|
+
* production build: npm run build → папка client/dist
|
|
23
|
+
*
|
|
24
|
+
* GUIDE_PAGES.md §3.1
|
|
25
|
+
* =============================================================================
|
|
26
|
+
*/
|
|
6
27
|
export default defineConfig({
|
|
7
28
|
plugins: [react(), tailwindcss()],
|
|
8
29
|
server: {
|
|
@@ -32,7 +32,8 @@ function shouldSkipFile(relPath) {
|
|
|
32
32
|
const base = path.basename(relPath);
|
|
33
33
|
if (SKIP_ROOT_FILES.has(base)) return true;
|
|
34
34
|
if (base.endsWith('.tgz')) return true;
|
|
35
|
-
|
|
35
|
+
const norm = relPath.replace(/\\/g, '/');
|
|
36
|
+
if (norm === 'server/.env') return true;
|
|
36
37
|
return false;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -1,4 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: server/db/init.js
|
|
4
|
+
* npm run db:init — schema+seed+админ. Логин/пароль админа ЗДЕСЬ (seedAdmin).
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
1
7
|
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* ИНИЦИАЛИЗАЦИЯ БД: npm run db:init
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* Читает schema.sql + seed.sql, создаёт админа adminka/password
|
|
13
|
+
* ЗАМЕНИТЕ: login/password в seedAdmin(), ФИО и email админа
|
|
14
|
+
*
|
|
15
|
+
* КОМАНДА: из корня проекта npm run db:init
|
|
16
|
+
* 1) Читает schema.sql (таблицы)
|
|
17
|
+
* 2) Читает seed.sql (услуги)
|
|
18
|
+
* 3) seedAdmin() — UPSERT админа (ON CONFLICT login)
|
|
19
|
+
*
|
|
20
|
+
* Перед запуском: CREATE DATABASE в psql + DATABASE_URL в server/.env
|
|
21
|
+
* БАНКЕТАМ.НЕТ: login Admin26, password Demo20 в seedAdmin()
|
|
22
|
+
*
|
|
23
|
+
* README.md | GUIDE_PAGES.md §8.5
|
|
24
|
+
* =============================================================================
|
|
25
|
+
*/
|
|
2
26
|
import fs from 'fs';
|
|
3
27
|
import path from 'path';
|
|
4
28
|
import { fileURLToPath } from 'url';
|
|
@@ -17,7 +41,7 @@ async function runSqlFile(filename) {
|
|
|
17
41
|
|
|
18
42
|
async function seedAdmin() {
|
|
19
43
|
// --- Учётка админа по заданию ДЭ (п.5) — менять здесь ---
|
|
20
|
-
|
|
44
|
+
// БАНКЕТАМ.НЕТ: const login = 'Admin26'; const password = 'Demo20';
|
|
21
45
|
const login = 'adminka';
|
|
22
46
|
const password = 'password';
|
|
23
47
|
const hash = await bcrypt.hash(password, 10);
|
|
@@ -28,9 +52,9 @@ async function seedAdmin() {
|
|
|
28
52
|
ON CONFLICT (login) DO UPDATE SET
|
|
29
53
|
password_hash = EXCLUDED.password_hash,
|
|
30
54
|
role = 'admin'`,
|
|
31
|
-
[login, hash, 'Администратор системы', '+7(000)-000-00-00', 'admin@moynesam.local']
|
|
55
|
+
[login, hash, 'Администратор системы', '+7(000)-000-00-00', 'admin@moynesam.local'] // БАНКЕТАМ.НЕТ: email admin@banketam.net
|
|
32
56
|
);
|
|
33
|
-
console.log('Админ: логин adminka, пароль password');
|
|
57
|
+
console.log('Админ: логин adminka, пароль password'); // БАНКЕТАМ.НЕТ: Admin26 / Demo20
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
async function main() {
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: server/db/pool.js
|
|
4
|
+
* Подключение БД. Только server/.env → DATABASE_URL
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* pool.js — ПОДКЛЮЧЕНИЕ К PostgreSQL
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* Менять ТОЛЬКО server/.env → DATABASE_URL
|
|
13
|
+
* postgresql://ЛОГИН:ПАРОЛЬ@localhost:5432/ИМЯ_БАЗЫ
|
|
14
|
+
*
|
|
15
|
+
* БАНКЕТАМ.НЕТ: CREATE DATABASE banketam_net; и /banketam_net в URL
|
|
16
|
+
* Импорт pool везде: import { pool } from '../db/pool.js';
|
|
17
|
+
*
|
|
18
|
+
* GUIDE_PAGES.md §8 | README.md (разбор строки подключения)
|
|
19
|
+
* =============================================================================
|
|
20
|
+
*/
|
|
2
21
|
import pg from 'pg';
|
|
3
22
|
import dotenv from 'dotenv';
|
|
4
23
|
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- ФАЙЛ: server/db/schema.sql
|
|
3
|
+
-- Таблицы users, service_types, requests. Статусы CHECK. npm run db:init
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
|
|
6
|
+
-- =============================================================================
|
|
1
7
|
-- СХЕМА БД — при смене темы переименуйте БД и таблицы по смыслу предметной области
|
|
2
8
|
-- Тема сейчас: клининг «Мой Не Сам» → заявки на уборку (requests)
|
|
3
9
|
-- Другая тема: заявки → orders / bookings / applications
|
|
10
|
+
-- =============================================================================
|
|
11
|
+
-- БАНКЕТАМ.НЕТ: users — без изменений структуры (ФИО, телефон, email, login, password).
|
|
12
|
+
-- БАНКЕТАМ.НЕТ: service_types → помещения (зал, ресторан, веранды).
|
|
13
|
+
-- БАНКЕТАМ.НЕТ: requests → бронирования; address можно оставить или переименовать в comment_only.
|
|
14
|
+
-- БАНКЕТАМ.НЕТ: scheduled_at → дата начала банкета.
|
|
15
|
+
-- БАНКЕТАМ.НЕТ: статусы CHECK → 'new','in_progress','completed' (без cancelled при необходимости).
|
|
16
|
+
-- БАНКЕТАМ.НЕТ: Модуль 3 — ДОБАВИТЬ таблицу reviews (request_id, user_id, text, rating, created_at).
|
|
17
|
+
-- =============================================================================
|
|
4
18
|
|
|
5
19
|
CREATE TABLE IF NOT EXISTS users (
|
|
6
20
|
id SERIAL PRIMARY KEY,
|
|
@@ -14,6 +28,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
14
28
|
);
|
|
15
29
|
|
|
16
30
|
-- Справочник видов услуг — при другой теме замените названия и список в seed
|
|
31
|
+
-- БАНКЕТАМ.НЕТ: по смыслу «помещения», таблицу можно не переименовывать (меньше правок в SQL)
|
|
17
32
|
CREATE TABLE IF NOT EXISTS service_types (
|
|
18
33
|
id SERIAL PRIMARY KEY,
|
|
19
34
|
name VARCHAR(255) NOT NULL UNIQUE
|
|
@@ -21,6 +36,7 @@ CREATE TABLE IF NOT EXISTS service_types (
|
|
|
21
36
|
|
|
22
37
|
-- Заявки заказчика — ядро предметной области
|
|
23
38
|
-- Поля: address (адрес/комментарий), scheduled_at (дата услуги), payment_type cash|card
|
|
39
|
+
-- service_type_id ИЛИ custom_service — «иная услуга»; БАНКЕТАМ.НЕТ: только service_type_id
|
|
24
40
|
-- status: new → админ меняет на in_progress / completed (подписи в requests.js STATUS_LABELS)
|
|
25
41
|
CREATE TABLE IF NOT EXISTS requests (
|
|
26
42
|
id SERIAL PRIMARY KEY,
|
|
@@ -35,6 +51,7 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
35
51
|
CHECK (status IN ('new', 'in_progress', 'completed', 'cancelled')),
|
|
36
52
|
cancel_reason TEXT, -- БАНКЕТАМ.НЕТ: поле можно не использовать (в задании нет «отмены»)
|
|
37
53
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
54
|
+
-- БАНКЕТАМ.НЕТ: ALTER TABLE requests ADD COLUMN review_text TEXT; — отзыв после смены статуса админом
|
|
38
55
|
);
|
|
39
56
|
|
|
40
57
|
CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- ФАЙЛ: server/db/seed.sql
|
|
3
|
+
-- Список услуг в service_types. Менять VALUES под тему.
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
|
|
6
|
+
-- =============================================================================
|
|
1
7
|
-- SEED — начальные записи в service_types (справочник для формы п.4)
|
|
8
|
+
-- =============================================================================
|
|
2
9
|
-- Выполняется из init.js при npm run db:init
|
|
3
10
|
-- ON CONFLICT DO NOTHING — повторный init не дублирует строки
|
|
4
11
|
--
|
|
5
12
|
-- Синхронизируйте с client/src/constants/services.js (лендинг)
|
|
13
|
+
-- БАНКЕТАМ.НЕТ: замените VALUES на четыре помещения:
|
|
6
14
|
-- ('Банкетный зал'), ('Ресторан'), ('Летняя веранда'), ('Закрытая веранда')
|
|
7
15
|
INSERT INTO service_types (name) VALUES
|
|
8
16
|
('Общий клининг'),
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: server/index.js
|
|
4
|
+
* Старт API :3001. Роуты /api/auth, /services, /requests, /admin
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* EXPRESS — точка входа сервера
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* PORT, DATABASE_URL, JWT_SECRET — в server/.env
|
|
13
|
+
* Префикс /api обычно не меняют. Роуты подключаются из папки routes/
|
|
14
|
+
* БАНКЕТАМ.НЕТ: DATABASE_URL в .env — имя БД например banketam_net
|
|
15
|
+
*
|
|
16
|
+
* ЗАПУСК: npm run dev (корень) или node index.js из server/
|
|
17
|
+
* Порядок: dotenv → cors → express.json() → роуты → listen(PORT)
|
|
18
|
+
*
|
|
19
|
+
* Новый роут: import + app.use('/api/xxx', xxxRoutes)
|
|
20
|
+
* Health-check: GET /api/health → { ok: true } — проверка что API жив
|
|
21
|
+
*
|
|
22
|
+
* GUIDE_PAGES.md §8 | README.md
|
|
23
|
+
* =============================================================================
|
|
24
|
+
*/
|
|
2
25
|
import express from 'express';
|
|
3
26
|
import cors from 'cors';
|
|
4
27
|
import dotenv from 'dotenv';
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ФАЙЛ: server/middleware/auth.js
|
|
4
|
+
* JWT. authRequired — user routes. adminRequired — /api/admin
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* JWT middleware
|
|
11
|
+
* =============================================================================
|
|
12
|
+
* JWT_SECRET — server/.env
|
|
13
|
+
* role в токене: user | admin — из таблицы users
|
|
14
|
+
*
|
|
15
|
+
* authRequired — вешать на роуты /requests, /admin (заголовок Authorization: Bearer ...)
|
|
16
|
+
* adminRequired — только для /api/admin/* (роль admin после входа Admin26)
|
|
17
|
+
*
|
|
18
|
+
* На фронте: ProtectedRoute role="user" | "admin" (client/src/components/ProtectedRoute.jsx)
|
|
19
|
+
*
|
|
20
|
+
* JWT_SECRET в server/.env — обязательно задать перед экзаменом
|
|
21
|
+
* =============================================================================
|
|
22
|
+
*/
|
|
2
23
|
import jwt from 'jsonwebtoken';
|
|
3
24
|
|
|
4
25
|
export function authRequired(req, res, next) {
|