@mashka818/exam-de-template 1.0.0

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.
Files changed (131) hide show
  1. package/exam-guides/COMMENTS_GUIDE.md +53 -0
  2. package/exam-guides/EXAM_COMMANDS.txt +47 -0
  3. package/exam-guides/GUIDE_PAGES.md +529 -0
  4. package/exam-guides/NPM_PACKAGE.md +206 -0
  5. package/exam-guides/README.md +40 -0
  6. package/exam-guides/RESPONSIVE.md +224 -0
  7. package/exam-guides/TECH_STACK.txt +142 -0
  8. package/exam-guides/THEME_BANQUETAM_NET.md +106 -0
  9. package/exam-guides/commented-code/README.txt +5 -0
  10. package/exam-guides/commented-code/client/index.html +14 -0
  11. package/exam-guides/commented-code/client/package-lock.json +2298 -0
  12. package/exam-guides/commented-code/client/package.json +21 -0
  13. package/exam-guides/commented-code/client/public/images/README.txt +26 -0
  14. package/exam-guides/commented-code/client/public/images/about-cleaning.svg +4 -0
  15. package/exam-guides/commented-code/client/public/images/admin-banner.svg +1 -0
  16. package/exam-guides/commented-code/client/public/images/empty-requests.svg +1 -0
  17. package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +4 -0
  18. package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +4 -0
  19. package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +4 -0
  20. package/exam-guides/commented-code/client/public/images/home-hero.svg +4 -0
  21. package/exam-guides/commented-code/client/public/images/login-banner.svg +1 -0
  22. package/exam-guides/commented-code/client/public/images/logo.svg +4 -0
  23. package/exam-guides/commented-code/client/public/images/new-request-banner.svg +1 -0
  24. package/exam-guides/commented-code/client/public/images/register-banner.svg +1 -0
  25. package/exam-guides/commented-code/client/public/images/requests-banner.svg +1 -0
  26. package/exam-guides/commented-code/client/public/images/slide-1.svg +6 -0
  27. package/exam-guides/commented-code/client/public/images/slide-2.svg +5 -0
  28. package/exam-guides/commented-code/client/public/images/slide-3.svg +5 -0
  29. package/exam-guides/commented-code/client/src/App.jsx +72 -0
  30. package/exam-guides/commented-code/client/src/api.js +71 -0
  31. package/exam-guides/commented-code/client/src/components/FormField.jsx +25 -0
  32. package/exam-guides/commented-code/client/src/components/Layout.jsx +83 -0
  33. package/exam-guides/commented-code/client/src/components/PageImage.jsx +38 -0
  34. package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +35 -0
  35. package/exam-guides/commented-code/client/src/components/UserNav.jsx +33 -0
  36. package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +103 -0
  37. package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +76 -0
  38. package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +74 -0
  39. package/exam-guides/commented-code/client/src/config/images.js +104 -0
  40. package/exam-guides/commented-code/client/src/constants/services.js +19 -0
  41. package/exam-guides/commented-code/client/src/context/AuthContext.jsx +72 -0
  42. package/exam-guides/commented-code/client/src/index.css +73 -0
  43. package/exam-guides/commented-code/client/src/main.jsx +28 -0
  44. package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +151 -0
  45. package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +131 -0
  46. package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +81 -0
  47. package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +117 -0
  48. package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +196 -0
  49. package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +112 -0
  50. package/exam-guides/commented-code/client/src/utils/validation.js +71 -0
  51. package/exam-guides/commented-code/client/vite.config.js +31 -0
  52. package/exam-guides/commented-code/server/db/init.js +67 -0
  53. package/exam-guides/commented-code/server/db/pool.js +23 -0
  54. package/exam-guides/commented-code/server/db/schema.sql +53 -0
  55. package/exam-guides/commented-code/server/db/seed.sql +15 -0
  56. package/exam-guides/commented-code/server/index.js +45 -0
  57. package/exam-guides/commented-code/server/middleware/auth.js +38 -0
  58. package/exam-guides/commented-code/server/package-lock.json +1084 -0
  59. package/exam-guides/commented-code/server/package.json +17 -0
  60. package/exam-guides/commented-code/server/routes/admin.js +96 -0
  61. package/exam-guides/commented-code/server/routes/auth.js +128 -0
  62. package/exam-guides/commented-code/server/routes/requests.js +115 -0
  63. package/exam-guides/commented-code/server/routes/services.js +31 -0
  64. package/exam-guides/commented-code/server/utils/validation.js +81 -0
  65. package/exam-guides/exam-starter/README.txt +22 -0
  66. package/exam-guides/exam-starter/package.json +13 -0
  67. package/exam-guides//320/243/320/224/320/220/320/233/320/230/320/242/320/254-/320/237/320/225/320/240/320/225/320/224-/320/241/320/224/320/220/320/247/320/225/320/231.txt +9 -0
  68. package/exam-project/README.md +16 -0
  69. package/exam-project/client/index.html +14 -0
  70. package/exam-project/client/package-lock.json +2298 -0
  71. package/exam-project/client/package.json +21 -0
  72. package/exam-project/client/public/images/README.txt +26 -0
  73. package/exam-project/client/public/images/about-cleaning.svg +4 -0
  74. package/exam-project/client/public/images/admin-banner.svg +1 -0
  75. package/exam-project/client/public/images/empty-requests.svg +1 -0
  76. package/exam-project/client/public/images/footer-photo-1.svg +4 -0
  77. package/exam-project/client/public/images/footer-photo-2.svg +4 -0
  78. package/exam-project/client/public/images/footer-photo-3.svg +4 -0
  79. package/exam-project/client/public/images/home-hero.svg +4 -0
  80. package/exam-project/client/public/images/login-banner.svg +1 -0
  81. package/exam-project/client/public/images/logo.svg +4 -0
  82. package/exam-project/client/public/images/new-request-banner.svg +1 -0
  83. package/exam-project/client/public/images/register-banner.svg +1 -0
  84. package/exam-project/client/public/images/requests-banner.svg +1 -0
  85. package/exam-project/client/public/images/slide-1.svg +6 -0
  86. package/exam-project/client/public/images/slide-2.svg +5 -0
  87. package/exam-project/client/public/images/slide-3.svg +5 -0
  88. package/exam-project/client/src/App.jsx +52 -0
  89. package/exam-project/client/src/api.js +50 -0
  90. package/exam-project/client/src/components/FormField.jsx +11 -0
  91. package/exam-project/client/src/components/Layout.jsx +61 -0
  92. package/exam-project/client/src/components/PageImage.jsx +22 -0
  93. package/exam-project/client/src/components/ProtectedRoute.jsx +20 -0
  94. package/exam-project/client/src/components/UserNav.jsx +20 -0
  95. package/exam-project/client/src/components/landing/HeroSlider.jsx +89 -0
  96. package/exam-project/client/src/components/landing/LandingLayout.jsx +61 -0
  97. package/exam-project/client/src/components/landing/SiteFooter.jsx +61 -0
  98. package/exam-project/client/src/config/images.js +83 -0
  99. package/exam-project/client/src/constants/services.js +7 -0
  100. package/exam-project/client/src/context/AuthContext.jsx +55 -0
  101. package/exam-project/client/src/index.css +54 -0
  102. package/exam-project/client/src/main.jsx +17 -0
  103. package/exam-project/client/src/pages/AdminPage.jsx +128 -0
  104. package/exam-project/client/src/pages/LandingPage.jsx +115 -0
  105. package/exam-project/client/src/pages/LoginPage.jsx +63 -0
  106. package/exam-project/client/src/pages/RegisterPage.jsx +97 -0
  107. package/exam-project/client/src/pages/RequestFormPage.jsx +178 -0
  108. package/exam-project/client/src/pages/RequestsPage.jsx +94 -0
  109. package/exam-project/client/src/utils/validation.js +54 -0
  110. package/exam-project/client/vite.config.js +17 -0
  111. package/exam-project/package.json +18 -0
  112. package/exam-project/scripts/init-project.js +86 -0
  113. package/exam-project/scripts/prepack.js +27 -0
  114. package/exam-project/scripts/unpack-template.js +105 -0
  115. package/exam-project/server/db/init.js +50 -0
  116. package/exam-project/server/db/pool.js +11 -0
  117. package/exam-project/server/db/schema.sql +41 -0
  118. package/exam-project/server/db/seed.sql +12 -0
  119. package/exam-project/server/index.js +29 -0
  120. package/exam-project/server/middleware/auth.js +24 -0
  121. package/exam-project/server/package-lock.json +1084 -0
  122. package/exam-project/server/package.json +17 -0
  123. package/exam-project/server/routes/admin.js +76 -0
  124. package/exam-project/server/routes/auth.js +109 -0
  125. package/exam-project/server/routes/requests.js +99 -0
  126. package/exam-project/server/routes/services.js +18 -0
  127. package/exam-project/server/utils/validation.js +63 -0
  128. package/package.json +25 -0
  129. package/scripts/init-project.js +86 -0
  130. package/scripts/prepack.js +27 -0
  131. package/scripts/unpack-template.js +105 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "exam-template-server",
3
+ "type": "module",
4
+ "scripts": {
5
+ "dev": "node --watch index.js",
6
+ "start": "node index.js",
7
+ "db:init": "node db/init.js"
8
+ },
9
+ "dependencies": {
10
+ "bcryptjs": "^3.0.2",
11
+ "cors": "^2.8.5",
12
+ "dotenv": "^16.5.0",
13
+ "express": "^5.1.0",
14
+ "jsonwebtoken": "^9.0.2",
15
+ "pg": "^8.16.0"
16
+ }
17
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * =============================================================================
3
+ * ADMIN — /api/admin (только role=admin, логин adminka)
4
+ * =============================================================================
5
+ * GET /requests — все заявки всех пользователей
6
+ * PATCH /requests/:id/status — in_progress | completed | cancelled + cancelReason
7
+ * БАНКЕТАМ.НЕТ (п.5): Admin26 / Demo20; ALLOWED_STATUSES = ['in_progress','completed'] без cancelled.
8
+ * БАНКЕТАМ.НЕТ: подписи «Банкет назначен» / «Банкет завершен»; изначально «Новая».
9
+ * БАНКЕТАМ.НЕТ (М2): фильтр ?status=, сортировка ORDER BY, LIMIT/OFFSET пагинация — в GET /requests.
10
+ *
11
+ * adminRequired — только login с role=admin из init.js
12
+ * PATCH body: { status, cancelReason? } — cancelReason только для cancelled
13
+ * После UPDATE — фронт перезагружает список load()
14
+ *
15
+ * Пример фильтра в SQL:
16
+ * const status = req.query.status;
17
+ * WHERE ($1::text IS NULL OR r.status = $1)
18
+ *
19
+ * GUIDE_PAGES.md §8.4 | AdminPage.jsx
20
+ * =============================================================================
21
+ */
22
+ import { Router } from 'express';
23
+ import { pool } from '../db/pool.js';
24
+ import { authRequired, adminRequired } from '../middleware/auth.js';
25
+
26
+ const router = Router();
27
+
28
+ const ALLOWED_STATUSES = ['in_progress', 'completed', 'cancelled']; // БАНКЕТАМ.НЕТ: убрать 'cancelled'
29
+
30
+ // GET /api/admin/requests — все заявки для панели администратора
31
+ router.get('/requests', authRequired, adminRequired, async (_req, res) => {
32
+ try {
33
+ const { rows } = await pool.query(
34
+ `SELECT r.*, st.name AS service_name, u.full_name AS user_full_name,
35
+ u.phone AS user_phone, u.email AS user_email
36
+ FROM requests r
37
+ LEFT JOIN service_types st ON st.id = r.service_type_id
38
+ JOIN users u ON u.id = r.user_id
39
+ ORDER BY r.created_at DESC`
40
+ );
41
+ res.json(
42
+ rows.map((row) => ({
43
+ id: row.id,
44
+ userFullName: row.user_full_name,
45
+ userPhone: row.user_phone,
46
+ userEmail: row.user_email,
47
+ address: row.address,
48
+ contactPhone: row.contact_phone,
49
+ serviceName: row.service_name || row.custom_service,
50
+ scheduledAt: row.scheduled_at,
51
+ paymentType: row.payment_type,
52
+ paymentLabel: row.payment_type === 'cash' ? 'Наличные' : 'Банковская карта',
53
+ status: row.status,
54
+ statusLabel:
55
+ { new: 'Новая', in_progress: 'В работе', completed: 'Выполнено', cancelled: 'Отменено' }[
56
+ row.status
57
+ ] || row.status,
58
+ cancelReason: row.cancel_reason,
59
+ createdAt: row.created_at,
60
+ }))
61
+ );
62
+ } catch (err) {
63
+ console.error(err);
64
+ res.status(500).json({ message: 'Ошибка сервера' });
65
+ }
66
+ });
67
+
68
+ // PATCH /api/admin/requests/:id/status — смена статуса админом
69
+ router.patch('/requests/:id/status', authRequired, adminRequired, async (req, res) => {
70
+ const { status, cancelReason } = req.body;
71
+ const id = Number(req.params.id);
72
+
73
+ if (!ALLOWED_STATUSES.includes(status)) {
74
+ return res.status(400).json({ message: 'Недопустимый статус' });
75
+ }
76
+ if (status === 'cancelled' && !cancelReason?.trim()) {
77
+ return res.status(400).json({ message: 'Укажите причину отмены' });
78
+ }
79
+
80
+ try {
81
+ const { rows } = await pool.query(
82
+ `UPDATE requests
83
+ SET status = $1, cancel_reason = $2
84
+ WHERE id = $3
85
+ RETURNING *`,
86
+ [status, status === 'cancelled' ? cancelReason.trim() : null, id]
87
+ );
88
+ if (!rows[0]) return res.status(404).json({ message: 'Заявка не найдена' });
89
+ res.json({ ok: true });
90
+ } catch (err) {
91
+ console.error(err);
92
+ res.status(500).json({ message: 'Ошибка сервера' });
93
+ }
94
+ });
95
+
96
+ export default router;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * =============================================================================
3
+ * AUTH — /api/auth
4
+ * =============================================================================
5
+ * POST /register — новый user (поля: login, password, fullName, phone, email)
6
+ * POST /login — вход, возвращает JWT
7
+ * GET /me — текущий пользователь по токену
8
+ * ЗАМЕНИТЕ: INSERT в users при других полях регистрации
9
+ * БАНКЕТАМ.НЕТ (п.1–2): validateRegistration — логин/пароль по заданию; ошибка «Логин уже занят».
10
+ *
11
+ * register: bcrypt.hash → INSERT users → JWT (role user)
12
+ * login: SELECT по login → bcrypt.compare → JWT { id, login, role }
13
+ * me: authRequired не здесь — см. ниже GET /me с jwt.verify в этом файле
14
+ *
15
+ * Ответ register/login: { token, user: { id, login, fullName, role, … } }
16
+ * Ошибка 400: { errors: { login: '...' } } — фронт кладёт в errors state
17
+ *
18
+ * GUIDE_PAGES.md §8.1 | client/pages/RegisterPage, LoginPage
19
+ * =============================================================================
20
+ */
21
+ import { Router } from 'express';
22
+ import bcrypt from 'bcryptjs';
23
+ import jwt from 'jsonwebtoken';
24
+ import { pool } from '../db/pool.js';
25
+ import { validateRegistration } from '../utils/validation.js';
26
+
27
+ const router = Router();
28
+
29
+ // POST /api/auth/register — регистрация заказчика
30
+ router.post('/register', async (req, res) => {
31
+ const { login, password, fullName, phone, email } = req.body;
32
+ const errors = validateRegistration({ login, password, fullName, phone, email });
33
+ if (Object.keys(errors).length) {
34
+ return res.status(400).json({ errors });
35
+ }
36
+
37
+ try {
38
+ const exists = await pool.query('SELECT id FROM users WHERE login = $1', [login.trim()]);
39
+ if (exists.rows.length) {
40
+ return res.status(400).json({ errors: { login: 'Логин уже занят' } });
41
+ }
42
+
43
+ const passwordHash = await bcrypt.hash(password, 10);
44
+ const result = await pool.query(
45
+ `INSERT INTO users (login, password_hash, full_name, phone, email)
46
+ VALUES ($1, $2, $3, $4, $5)
47
+ RETURNING id, login, full_name, phone, email, role`,
48
+ [login.trim(), passwordHash, fullName.trim(), phone.trim(), email.trim()]
49
+ );
50
+
51
+ const user = result.rows[0];
52
+ const token = signToken(user);
53
+ res.status(201).json({ user: mapUser(user), token });
54
+ } catch (err) {
55
+ console.error(err);
56
+ res.status(500).json({ message: 'Ошибка сервера' });
57
+ }
58
+ });
59
+
60
+ // POST /api/auth/login — вход (и обычный пользователь, и adminka)
61
+ router.post('/login', async (req, res) => {
62
+ const { login, password } = req.body;
63
+ if (!login?.trim() || !password) {
64
+ return res.status(400).json({ message: 'Введите логин и пароль' });
65
+ }
66
+
67
+ try {
68
+ const result = await pool.query(
69
+ 'SELECT id, login, password_hash, full_name, phone, email, role FROM users WHERE login = $1',
70
+ [login.trim()]
71
+ );
72
+ const user = result.rows[0];
73
+ if (!user) {
74
+ return res.status(401).json({ message: 'Неверный логин или пароль' });
75
+ }
76
+
77
+ const ok = await bcrypt.compare(password, user.password_hash);
78
+ if (!ok) {
79
+ return res.status(401).json({ message: 'Неверный логин или пароль' });
80
+ }
81
+
82
+ const token = signToken(user);
83
+ res.json({ user: mapUser(user), token });
84
+ } catch (err) {
85
+ console.error(err);
86
+ res.status(500).json({ message: 'Ошибка сервера' });
87
+ }
88
+ });
89
+
90
+ // GET /api/auth/me — текущий пользователь по токену
91
+ router.get('/me', async (req, res) => {
92
+ const header = req.headers.authorization;
93
+ if (!header?.startsWith('Bearer ')) {
94
+ return res.status(401).json({ message: 'Не авторизован' });
95
+ }
96
+ try {
97
+ const payload = jwt.verify(header.slice(7), process.env.JWT_SECRET);
98
+ const result = await pool.query(
99
+ 'SELECT id, login, full_name, phone, email, role FROM users WHERE id = $1',
100
+ [payload.id]
101
+ );
102
+ if (!result.rows[0]) return res.status(401).json({ message: 'Пользователь не найден' });
103
+ res.json({ user: mapUser(result.rows[0]) });
104
+ } catch {
105
+ res.status(401).json({ message: 'Недействительный токен' });
106
+ }
107
+ });
108
+
109
+ function signToken(user) {
110
+ return jwt.sign(
111
+ { id: user.id, login: user.login, role: user.role },
112
+ process.env.JWT_SECRET,
113
+ { expiresIn: '24h' }
114
+ );
115
+ }
116
+
117
+ function mapUser(row) {
118
+ return {
119
+ id: row.id,
120
+ login: row.login,
121
+ fullName: row.full_name,
122
+ phone: row.phone,
123
+ email: row.email,
124
+ role: row.role,
125
+ };
126
+ }
127
+
128
+ export default router;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * =============================================================================
3
+ * REQUESTS — /api/requests (только role=user)
4
+ * =============================================================================
5
+ * GET /mine — история заявок текущего пользователя
6
+ * POST / — новая заявка
7
+ * STATUS_LABELS — подписи статусов на русском для фронта
8
+ * БАНКЕТАМ.НЕТ: in_progress → «Банкет назначен», completed → «Банкет завершен».
9
+ * БАНКЕТАМ.НЕТ: POST /api/requests/reviews/:id — отзыв (только если status !== 'new').
10
+ *
11
+ * mapRequest — snake_case из PG → camelCase для React (scheduledAt, serviceName…)
12
+ * POST /: body как на RequestFormPage; validateRequest в utils/validation.js
13
+ * user_id берётся из JWT (req.user.id), клиент не передаёт userId
14
+ *
15
+ * GUIDE_PAGES.md §8.2 | schema.sql таблица requests
16
+ * =============================================================================
17
+ */
18
+ import { Router } from 'express';
19
+ import { pool } from '../db/pool.js';
20
+ import { authRequired } from '../middleware/auth.js';
21
+ import { validateRequest } from '../utils/validation.js';
22
+
23
+ const router = Router();
24
+
25
+ const STATUS_LABELS = {
26
+ new: 'Новая', // БАНКЕТАМ.НЕТ: без изменений
27
+ in_progress: 'В работе', // БАНКЕТАМ.НЕТ: 'Банкет назначен'
28
+ completed: 'Выполнено', // БАНКЕТАМ.НЕТ: 'Банкет завершен'
29
+ cancelled: 'Отменено', // БАНКЕТАМ.НЕТ: можно убрать
30
+ };
31
+
32
+ // GET /api/requests/mine — история заявок текущего пользователя
33
+ router.get('/mine', authRequired, async (req, res) => {
34
+ try {
35
+ const { rows } = await pool.query(
36
+ `SELECT r.*, st.name AS service_name, u.full_name AS user_full_name
37
+ FROM requests r
38
+ LEFT JOIN service_types st ON st.id = r.service_type_id
39
+ JOIN users u ON u.id = r.user_id
40
+ WHERE r.user_id = $1
41
+ ORDER BY r.created_at DESC`,
42
+ [req.user.id]
43
+ );
44
+ res.json(rows.map(mapRequest));
45
+ } catch (err) {
46
+ console.error(err);
47
+ res.status(500).json({ message: 'Ошибка сервера' });
48
+ }
49
+ });
50
+
51
+ // POST /api/requests — новая заявка
52
+ router.post('/', authRequired, async (req, res) => {
53
+ if (req.user.role === 'admin') {
54
+ return res.status(403).json({ message: 'Администратор не создаёт заявки через этот endpoint' });
55
+ }
56
+
57
+ const body = {
58
+ address: req.body.address,
59
+ contactPhone: req.body.contactPhone,
60
+ scheduledAt: req.body.scheduledAt,
61
+ paymentType: req.body.paymentType,
62
+ serviceTypeId: req.body.serviceTypeId,
63
+ isCustomService: req.body.isCustomService,
64
+ customService: req.body.customService,
65
+ };
66
+
67
+ const errors = validateRequest(body);
68
+ if (Object.keys(errors).length) {
69
+ return res.status(400).json({ errors });
70
+ }
71
+
72
+ try {
73
+ const { rows } = await pool.query(
74
+ `INSERT INTO requests (
75
+ user_id, address, contact_phone, service_type_id, custom_service,
76
+ scheduled_at, payment_type, status
77
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'new')
78
+ RETURNING *`,
79
+ [
80
+ req.user.id,
81
+ body.address.trim(),
82
+ body.contactPhone.trim(),
83
+ body.isCustomService ? null : Number(body.serviceTypeId),
84
+ body.isCustomService ? body.customService.trim() : null,
85
+ body.scheduledAt,
86
+ body.paymentType,
87
+ ]
88
+ );
89
+ res.status(201).json(mapRequest(rows[0]));
90
+ } catch (err) {
91
+ console.error(err);
92
+ res.status(500).json({ message: 'Ошибка сервера' });
93
+ }
94
+ });
95
+
96
+ function mapRequest(row) {
97
+ return {
98
+ id: row.id,
99
+ userId: row.user_id,
100
+ userFullName: row.user_full_name,
101
+ address: row.address,
102
+ contactPhone: row.contact_phone,
103
+ serviceName: row.service_name || row.custom_service,
104
+ customService: row.custom_service,
105
+ scheduledAt: row.scheduled_at,
106
+ paymentType: row.payment_type,
107
+ paymentLabel: row.payment_type === 'cash' ? 'Наличные' : 'Банковская карта',
108
+ status: row.status,
109
+ statusLabel: STATUS_LABELS[row.status] || row.status,
110
+ cancelReason: row.cancel_reason,
111
+ createdAt: row.created_at,
112
+ };
113
+ }
114
+
115
+ export default router;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * =============================================================================
3
+ * SERVICES — GET /api/services
4
+ * =============================================================================
5
+ * Справочник service_types — заполняется при npm run db:init из seed.sql.
6
+ * Фронт: RequestFormPage useEffect → api.getServices() → <select>.
7
+ *
8
+ * Менять список: server/db/seed.sql (пересоздать БД или UPDATE вручную).
9
+ * БАНКЕТАМ.НЕТ: id + name помещений (зал, ресторан, веранды…)
10
+ *
11
+ * Авторизация не нужна — справочник публичный.
12
+ * GUIDE_PAGES.md §8.3 | seed.sql
13
+ * =============================================================================
14
+ */
15
+ import { Router } from 'express';
16
+ import { pool } from '../db/pool.js';
17
+
18
+ const router = Router();
19
+
20
+ // GET /api/services — список видов услуг для select на форме заявки
21
+ router.get('/', async (_req, res) => {
22
+ try {
23
+ const { rows } = await pool.query('SELECT id, name FROM service_types ORDER BY id');
24
+ res.json(rows);
25
+ } catch (err) {
26
+ console.error(err);
27
+ res.status(500).json({ message: 'Ошибка сервера' });
28
+ }
29
+ });
30
+
31
+ export default router;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * =============================================================================
3
+ * ВАЛИДАЦИЯ НА СЕРВЕРЕ (обязательно дублировать на клиенте)
4
+ * =============================================================================
5
+ * ЗАМЕНИТЕ регулярки и тексты ошибок по критериям задания.
6
+ * =============================================================================
7
+ * БАНКЕТАМ.НЕТ (п.1): логин — латиница и цифры, min 6: LOGIN_REGEX = /^[a-zA-Z0-9]{6,}$/
8
+ * БАНКЕТАМ.НЕТ (п.1): пароль min 8 символов (сейчас 6).
9
+ * БАНКЕТАМ.НЕТ (п.4): дата ДД.ММ.ГГГГ — DATE_REGEX + parse в validateRequest.
10
+ * БАНКЕТАМ.НЕТ: убрать проверку isCustomService / custom_service если нет «иной услуги».
11
+ *
12
+ * Вызывается из routes перед INSERT — никогда не доверяйте только клиенту.
13
+ * Возвращает объект errors {}; пустой = ок. Роут отвечает res.status(400).json({ errors })
14
+ *
15
+ * Дубль на клиенте: client/src/utils/validation.js (те же REGEX и тексты)
16
+ *
17
+ * GUIDE_PAGES.md §7.1 | RegisterPage + RequestFormPage
18
+ * =============================================================================
19
+ */
20
+
21
+ /** Формат телефона из задания — синхронизировать с formatPhoneInput на фронте */
22
+ export const PHONE_REGEX = /^\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}$/;
23
+ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24
+ export const FIO_REGEX = /^[А-Яа-яЁё\s-]+$/;
25
+
26
+ /** POST /api/auth/register */
27
+ export function validateRegistration({ login, password, fullName, phone, email }) {
28
+ const errors = {};
29
+
30
+ if (!login?.trim()) errors.login = 'Логин обязателен';
31
+ else if (login.trim().length < 3) errors.login = 'Логин не короче 3 символов'; // БАНКЕТАМ.НЕТ: min 6 + только [a-zA-Z0-9]
32
+
33
+ if (!password) errors.password = 'Пароль обязателен';
34
+ else if (password.length < 6) errors.password = 'Пароль не менее 6 символов'; // БАНКЕТАМ.НЕТ: < 8
35
+
36
+ if (!fullName?.trim()) errors.fullName = 'ФИО обязательно';
37
+ else if (!FIO_REGEX.test(fullName.trim())) errors.fullName = 'ФИО: только кириллица и пробелы';
38
+
39
+ if (!phone?.trim()) errors.phone = 'Телефон обязателен';
40
+ else if (!PHONE_REGEX.test(phone.trim())) errors.phone = 'Формат: +7(XXX)-XXX-XX-XX';
41
+
42
+ if (!email?.trim()) errors.email = 'Email обязателен';
43
+ else if (!EMAIL_REGEX.test(email.trim())) errors.email = 'Некорректный email';
44
+
45
+ return errors;
46
+ }
47
+
48
+ /** POST /api/requests */
49
+ export function validateRequest(body) {
50
+ const errors = {};
51
+ const {
52
+ address,
53
+ contactPhone,
54
+ scheduledAt,
55
+ paymentType,
56
+ serviceTypeId,
57
+ isCustomService,
58
+ customService,
59
+ } = body;
60
+
61
+ if (!address?.trim()) errors.address = 'Адрес обязателен';
62
+ if (!contactPhone?.trim()) errors.contactPhone = 'Контактный телефон обязателен';
63
+ else if (!PHONE_REGEX.test(contactPhone.trim()))
64
+ errors.contactPhone = 'Формат: +7(XXX)-XXX-XX-XX';
65
+
66
+ if (!scheduledAt) errors.scheduledAt = 'Дата и время обязательны';
67
+ else if (new Date(scheduledAt) <= new Date())
68
+ errors.scheduledAt = 'Дата должна быть в будущем';
69
+
70
+ if (!paymentType) errors.paymentType = 'Укажите способ оплаты';
71
+ else if (!['cash', 'card'].includes(paymentType))
72
+ errors.paymentType = 'Некорректный способ оплаты';
73
+
74
+ if (isCustomService) {
75
+ if (!customService?.trim()) errors.customService = 'Опишите иную услугу';
76
+ } else if (!serviceTypeId) {
77
+ errors.serviceTypeId = 'Выберите вид услуги';
78
+ }
79
+
80
+ return errors;
81
+ }
@@ -0,0 +1,22 @@
1
+ ЭКЗАМЕН БЕЗ ФЛЕШКИ
2
+ ==================
3
+
4
+ Флешка не нужна. Шаблон качается с https://www.npmjs.com/ (интернет на экзамене).
5
+
6
+ 1) ДОМА: npm login → npm publish (пакет @mashka818/exam-de-template)
7
+ Подробно: NPM_PACKAGE.md
8
+
9
+ 2) НА ЭКЗАМЕНЕ (проще всего):
10
+
11
+ mkdir C:\DE\work
12
+ cd C:\DE\work
13
+ npx @mashka818/exam-de-template@1.0.0 init
14
+
15
+ copy server\.env.example server\.env
16
+ npm run db:init
17
+ npm run dev
18
+
19
+ 3) Через npm install — см. EXAM_COMMANDS.txt (способы 2 и 3)
20
+
21
+ Файл package.json в этой папке — образец для способа 3 (вручную).
22
+ Пакет: @mashka818/exam-de-template
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "de-exam-work",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Пример package.json (основной способ — npx init, см. EXAM_COMMANDS.txt)",
6
+ "scripts": {
7
+ "postinstall": "exam-de-unpack --here",
8
+ "setup": "npm install && npm run install:all"
9
+ },
10
+ "dependencies": {
11
+ "@mashka818/exam-de-template": "1.0.0"
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ ================================================================================
2
+ ПЕРЕД СДАЧЕЙ УДАЛИТЕ ВСЮ ПАПКУ exam-guides
3
+ ================================================================================
4
+
5
+ Оставьте только соседнюю папку: exam-project
6
+
7
+ ПКМ по exam-guides → Удалить
8
+
9
+ ================================================================================
@@ -0,0 +1,16 @@
1
+ # Проект для экзамена (сдаёте эту папку)
2
+
3
+ Чистый рабочий шаблон без шпор в коде.
4
+
5
+ ```powershell
6
+ npm run install:all
7
+ copy server\.env.example server\.env
8
+ npm run db:init
9
+ npm run dev
10
+ ```
11
+
12
+ Сайт: http://localhost:5173
13
+
14
+ **Перед сдачей:** удалите соседнюю папку **exam-guides** (целиком).
15
+
16
+ Дома обновить из копии с комментариями: в корне репозитория `npm run sync:project`
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <!-- ЗАМЕНИТЕ title под название портала вашей темы -->
7
+ <!-- БАНКЕТАМ.НЕТ: Банкетам.Нет — бронирование банкетных залов -->
8
+ <title>Мой Не Сам — портал клининговых услуг</title>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+ </html>