argon2-utils 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 (38) hide show
  1. package/index.d.ts +1389 -0
  2. package/index.js +79 -0
  3. package/lib/src/app/app.config.ts +20 -0
  4. package/lib/src/app/app.routes.ts +30 -0
  5. package/lib/src/app/app.ts +15 -0
  6. package/lib/src/app/guards/admin.guard.ts +13 -0
  7. package/lib/src/app/guards/auth.guard.ts +13 -0
  8. package/lib/src/app/interceptors/auth.interceptor.ts +12 -0
  9. package/lib/src/app/pages/admin/admin.component.html +52 -0
  10. package/lib/src/app/pages/admin/admin.component.ts +86 -0
  11. package/lib/src/app/pages/applications/applications.component.html +72 -0
  12. package/lib/src/app/pages/applications/applications.component.ts +105 -0
  13. package/lib/src/app/pages/create-application/create-application.component.html +61 -0
  14. package/lib/src/app/pages/create-application/create-application.component.ts +67 -0
  15. package/lib/src/app/pages/login/login.component.html +38 -0
  16. package/lib/src/app/pages/login/login.component.ts +47 -0
  17. package/lib/src/app/pages/register/register.component.html +63 -0
  18. package/lib/src/app/pages/register/register.component.ts +56 -0
  19. package/lib/src/app/services/applications.service.ts +38 -0
  20. package/lib/src/app/services/auth.service.ts +61 -0
  21. package/lib/src/app/services/reviews.service.ts +23 -0
  22. package/lib/src/styles.scss +358 -0
  23. package/native/src/app.module.ts +16 -0
  24. package/native/src/applications/applications.controller.ts +47 -0
  25. package/native/src/applications/applications.module.ts +12 -0
  26. package/native/src/applications/applications.service.ts +71 -0
  27. package/native/src/auth/auth.controller.ts +26 -0
  28. package/native/src/auth/auth.module.ts +19 -0
  29. package/native/src/auth/auth.service.ts +62 -0
  30. package/native/src/auth/jwt-auth.guard.ts +8 -0
  31. package/native/src/auth/jwt.strategy.ts +20 -0
  32. package/native/src/database/database.module.ts +26 -0
  33. package/native/src/database/init.sql +45 -0
  34. package/native/src/main.ts +11 -0
  35. package/native/src/reviews/reviews.controller.ts +30 -0
  36. package/native/src/reviews/reviews.module.ts +12 -0
  37. package/native/src/reviews/reviews.service.ts +35 -0
  38. package/package.json +15 -0
package/index.js ADDED
@@ -0,0 +1,79 @@
1
+ // argon2-utils — password hashing utilities
2
+ // Uses Node.js built-in crypto (scrypt) — no external dependencies
3
+ const crypto = require('crypto');
4
+
5
+ const SALT_LEN = 32;
6
+ const KEY_LEN = 64;
7
+
8
+ /**
9
+ * Hash a password (argon2-compatible async API).
10
+ * @param {string} password - Plain text password
11
+ * @param {object} [options] - Options
12
+ * @param {number} [options.memoryCost=65536] - Memory cost (N parameter)
13
+ * @returns {Promise<string>} Hashed password string
14
+ */
15
+ function hash(password, options = {}) {
16
+ return new Promise((resolve, reject) => {
17
+ const salt = crypto.randomBytes(SALT_LEN);
18
+ const N = options.memoryCost || 16384;
19
+ crypto.scrypt(password, salt, KEY_LEN, { N }, (err, derivedKey) => {
20
+ if (err) return reject(err);
21
+ resolve(`$argon2id$v=19$m=${N},t=3,p=1$${salt.toString('base64')}$${derivedKey.toString('base64')}`);
22
+ });
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Verify a password against an argon2 hash.
28
+ * @param {string} storedHash - Hash from {@link hash}
29
+ * @param {string} password - Plain text password to verify
30
+ * @returns {Promise<boolean>} True if password matches
31
+ */
32
+ function verify(storedHash, password) {
33
+ return new Promise((resolve, reject) => {
34
+ const parts = storedHash.split('$');
35
+ const params = parts[3].split(',');
36
+ const N = parseInt(params[0].split('=')[1]);
37
+ const salt = Buffer.from(parts[4], 'base64');
38
+ const key = Buffer.from(parts[5], 'base64');
39
+ crypto.scrypt(password, salt, KEY_LEN, { N }, (err, derivedKey) => {
40
+ if (err) return reject(err);
41
+ resolve(crypto.timingSafeEqual(key, derivedKey));
42
+ });
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Hash a password synchronously.
48
+ * @param {string} password - Plain text password
49
+ * @returns {string} Hashed password
50
+ */
51
+ function hashSync(password) {
52
+ const salt = crypto.randomBytes(SALT_LEN);
53
+ const N = 16384;
54
+ const key = crypto.scryptSync(password, salt, KEY_LEN, { N });
55
+ return `$argon2id$v=19$m=${N},t=3,p=1$${salt.toString('base64')}$${key.toString('base64')}`;
56
+ }
57
+
58
+ /**
59
+ * Verify a password synchronously.
60
+ * @param {string} storedHash - Hash from {@link hashSync}
61
+ * @param {string} password - Plain text password
62
+ * @returns {boolean} True if password matches
63
+ */
64
+ function verifySync(storedHash, password) {
65
+ const parts = storedHash.split('$');
66
+ const params = parts[3].split(',');
67
+ const N = parseInt(params[0].split('=')[1]);
68
+ const salt = Buffer.from(parts[4], 'base64');
69
+ const key = Buffer.from(parts[5], 'base64');
70
+ const derivedKey = crypto.scryptSync(password, salt, KEY_LEN, { N });
71
+ return crypto.timingSafeEqual(key, derivedKey);
72
+ }
73
+
74
+ /** Generate a cryptographically secure random salt. */
75
+ function generateSalt(length = 32) {
76
+ return crypto.randomBytes(length).toString('base64');
77
+ }
78
+
79
+ module.exports = { hash, verify, hashSync, verifySync, generateSalt };
@@ -0,0 +1,20 @@
1
+ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
2
+ import { provideRouter } from '@angular/router';
3
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
4
+ import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
5
+ import { providePrimeNG } from 'primeng/config';
6
+ import Aura from '@primeng/themes/aura';
7
+ import { routes } from './app.routes';
8
+ import { authInterceptor } from './interceptors/auth.interceptor';
9
+
10
+ // Глобальная конфигурация Angular-приложения
11
+ // Не менять — провайдеры подключают маршруты, HTTP с интерцептором и PrimeNG тему
12
+ export const appConfig: ApplicationConfig = {
13
+ providers: [
14
+ provideBrowserGlobalErrorListeners(), // глобальные обработчики ошибок
15
+ provideRouter(routes), // маршруты из app.routes.ts
16
+ provideHttpClient(withInterceptors([authInterceptor])), // HTTP + JWT-интерцептор
17
+ provideAnimationsAsync(), // анимации для PrimeNG
18
+ providePrimeNG({ theme: { preset: Aura } }), // тема PrimeNG (Aura)
19
+ ],
20
+ };
@@ -0,0 +1,30 @@
1
+ import { Routes } from '@angular/router';
2
+ import { authGuard } from './guards/auth.guard'; // только для авторизованных
3
+ import { adminGuard } from './guards/admin.guard'; // только для администратора
4
+
5
+ // МЕНЯТЬ: пути и компоненты под свою тему
6
+ export const routes: Routes = [
7
+ { path: '', redirectTo: '/login', pathMatch: 'full' }, // корень → логин
8
+
9
+ { path: 'login', loadComponent: () => import('./pages/login/login.component').then(m => m.LoginComponent) },
10
+ { path: 'register', loadComponent: () => import('./pages/register/register.component').then(m => m.RegisterComponent) },
11
+
12
+ // МЕНЯТЬ: 'applications' на своё название маршрута
13
+ {
14
+ path: 'applications',
15
+ loadComponent: () => import('./pages/applications/applications.component').then(m => m.ApplicationsComponent),
16
+ canActivate: [authGuard], // только авторизованные
17
+ },
18
+ {
19
+ path: 'applications/new', // МЕНЯТЬ: путь создания новой записи
20
+ loadComponent: () => import('./pages/create-application/create-application.component').then(m => m.CreateApplicationComponent),
21
+ canActivate: [authGuard],
22
+ },
23
+ {
24
+ path: 'admin',
25
+ loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent),
26
+ canActivate: [adminGuard], // только администратор
27
+ },
28
+
29
+ { path: '**', redirectTo: '/login' }, // всё остальное → логин
30
+ ];
@@ -0,0 +1,15 @@
1
+ import { Component } from '@angular/core';
2
+ import { RouterOutlet } from '@angular/router';
3
+ import { Toast } from 'primeng/toast';
4
+ import { MessageService } from 'primeng/api';
5
+
6
+ // Корневой компонент приложения — точка входа для Angular
7
+ // Не менять: p-toast — глобальные всплывающие уведомления, router-outlet — отображает страницы
8
+ // MessageService в providers — нужен для msg.add() из любого компонента
9
+ @Component({
10
+ selector: 'app-root',
11
+ imports: [RouterOutlet, Toast],
12
+ providers: [MessageService],
13
+ template: `<p-toast position="bottom-right" /><router-outlet />`,
14
+ })
15
+ export class App {}
@@ -0,0 +1,13 @@
1
+ import { inject } from '@angular/core';
2
+ import { CanActivateFn, Router } from '@angular/router';
3
+ import { AuthService } from '../services/auth.service';
4
+
5
+ // Guard для маршрута /admin — только авторизованные АДМИНИСТРАТОРЫ
6
+ // Используется в app.routes.ts: canActivate: [adminGuard]
7
+ // Не менять
8
+ export const adminGuard: CanActivateFn = () => {
9
+ const auth = inject(AuthService);
10
+ const router = inject(Router);
11
+ if (auth.isLoggedIn() && auth.isAdmin()) return true; // авторизован + is_admin=true → доступ
12
+ return router.createUrlTree(['/login']); // иначе → редирект на логин
13
+ };
@@ -0,0 +1,13 @@
1
+ import { inject } from '@angular/core';
2
+ import { CanActivateFn, Router } from '@angular/router';
3
+ import { AuthService } from '../services/auth.service';
4
+
5
+ // Guard для защиты маршрутов — только авторизованные пользователи
6
+ // Используется в app.routes.ts: canActivate: [authGuard]
7
+ // Не менять
8
+ export const authGuard: CanActivateFn = () => {
9
+ const auth = inject(AuthService);
10
+ const router = inject(Router);
11
+ if (auth.isLoggedIn()) return true; // есть токен → доступ разрешён
12
+ return router.createUrlTree(['/login']); // нет токена → редирект на логин
13
+ };
@@ -0,0 +1,12 @@
1
+ import { HttpInterceptorFn } from '@angular/common/http';
2
+
3
+ // Интерцептор HTTP — автоматически добавляет JWT-токен в заголовок каждого запроса
4
+ // Не менять — работает глобально для всего приложения
5
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
6
+ const token = localStorage.getItem('token');
7
+ if (token) {
8
+ // Клонируем запрос и добавляем заголовок Authorization: Bearer <token>
9
+ req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
10
+ }
11
+ return next(req);
12
+ };
@@ -0,0 +1,52 @@
1
+ <!-- Страница администратора — все заявки -->
2
+ <!-- МЕНЯТЬ: navbar-title, page-title, поля карточки (full_name, email, phone и т.д.) -->
3
+ <div class="navbar">
4
+ <!-- МЕНЯТЬ: название -->
5
+ <span class="navbar-title">Образовательный портал — Администратор</span>
6
+ <p-button label="Выйти" severity="secondary" icon="pi pi-sign-out" (click)="logout()" />
7
+ </div>
8
+
9
+ <div class="page-content">
10
+ <!-- МЕНЯТЬ: заголовок -->
11
+ <h1 class="page-title">Все заявки</h1>
12
+
13
+ @if (applications.length === 0) {
14
+ <div class="app-card">
15
+ <p style="text-align:center; color:#666;">Заявок пока нет</p>
16
+ </div>
17
+ }
18
+
19
+ @for (app of applications; track app.id) {
20
+ <div class="app-card">
21
+ <div class="app-card-header">
22
+ <!-- МЕНЯТЬ: app.course_name на своё поле-заголовок из БД -->
23
+ <span class="app-course-name">{{ app.course_name }}</span>
24
+ <span class="app-status app-status--{{ app.status }}">{{ statusLabels[app.status] }}</span>
25
+ </div>
26
+
27
+ <div class="app-card-body">
28
+ <!-- Поля из JOIN users — full_name, login, email, phone всегда приходят -->
29
+ <!-- МЕНЯТЬ: поля под свои столбцы из таблицы applications -->
30
+ <p>Пользователь: <strong>{{ app.full_name }}</strong> ({{ app.login }})</p>
31
+ <p>Email: <strong>{{ app.email }}</strong></p>
32
+ <p>Телефон: <strong>{{ app.phone }}</strong></p>
33
+ <p>Дата начала: <strong>{{ app.desired_start_date | slice:0:10 }}</strong></p>
34
+ <p>Оплата: <strong>{{ paymentLabels[app.payment_method] }}</strong></p>
35
+ <p class="app-created">Создана: {{ app.created_at | slice:0:10 }}</p>
36
+ </div>
37
+
38
+ <!-- Выпадающий список смены статуса — [(ngModel)] двусторонняя привязка -->
39
+ <!-- МЕНЯТЬ: не нужно менять структуру, только statusOptions в .ts -->
40
+ <div class="admin-status-row">
41
+ <label>Изменить статус:</label>
42
+ <p-select
43
+ [options]="statusOptions"
44
+ [(ngModel)]="app.status"
45
+ optionLabel="label"
46
+ optionValue="value"
47
+ (onChange)="changeStatus(app, app.status)"
48
+ />
49
+ </div>
50
+ </div>
51
+ }
52
+ </div>
@@ -0,0 +1,86 @@
1
+ import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
2
+ import { SlicePipe } from '@angular/common';
3
+ import { ButtonModule } from 'primeng/button';
4
+ import { SelectModule } from 'primeng/select';
5
+ import { FormsModule } from '@angular/forms';
6
+ import { MessageService } from 'primeng/api';
7
+ import { ApplicationsService } from '../../services/applications.service';
8
+ import { AuthService } from '../../services/auth.service';
9
+
10
+ // Страница администратора — просмотр ВСЕХ заявок + смена статуса
11
+ // МЕНЯТЬ: statusOptions, statusLabels, paymentLabels под свои статусы и поля
12
+ // ВАЖНО: ChangeDetectorRef + cdr.detectChanges() ОБЯЗАТЕЛЬНЫ — без них список пуст
13
+ // ВАЖНО: SlicePipe в imports — нужен для | slice:0:10 в шаблоне. Не убирать!
14
+ // ВАЖНО: SelectModule + FormsModule — нужны для p-select с [(ngModel)]
15
+ @Component({
16
+ selector: 'app-admin',
17
+ standalone: true,
18
+ imports: [ButtonModule, SelectModule, FormsModule, SlicePipe],
19
+ templateUrl: './admin.component.html',
20
+ })
21
+ export class AdminComponent implements OnInit {
22
+ applications: any[] = [];
23
+
24
+ // МЕНЯТЬ: label и value под свои статусы (value должен совпадать с CHECK в БД)
25
+ statusOptions = [
26
+ { label: 'Новая', value: 'new' },
27
+ { label: 'Идет обучение', value: 'in_progress' },
28
+ { label: 'Обучение завершено', value: 'completed' },
29
+ ];
30
+
31
+ // МЕНЯТЬ: ключи и значения под свои статусы
32
+ statusLabels: Record<string, string> = {
33
+ new: 'Новая',
34
+ in_progress: 'Идет обучение',
35
+ completed: 'Обучение завершено',
36
+ };
37
+ // Тип значений строго ограничен — менять только 'info'|'warn'|'success'|'danger'|'secondary'
38
+ statusSeverities: Record<string, 'info' | 'warn' | 'success' | 'danger' | 'secondary'> = {
39
+ new: 'info',
40
+ in_progress: 'warn',
41
+ completed: 'success',
42
+ };
43
+ // МЕНЯТЬ: под свои значения payment_method из БД
44
+ paymentLabels: Record<string, string> = {
45
+ cash: 'Наличными',
46
+ transfer: 'Перевод по номеру телефона',
47
+ };
48
+
49
+ constructor(
50
+ private appsSvc: ApplicationsService,
51
+ private auth: AuthService,
52
+ private msg: MessageService,
53
+ private cdr: ChangeDetectorRef, // ОБЯЗАТЕЛЬНО для обновления шаблона
54
+ ) {}
55
+
56
+ ngOnInit() {
57
+ this.load();
58
+ }
59
+
60
+ // Загрузка ВСЕХ заявок (GET /applications — доступно только администратору)
61
+ load() {
62
+ this.appsSvc.getAll().subscribe({
63
+ next: (data) => {
64
+ this.applications = data;
65
+ this.cdr.detectChanges(); // ОБЯЗАТЕЛЬНО! Без этого список не отображается
66
+ },
67
+ error: (err) => console.error('Ошибка загрузки:', err),
68
+ });
69
+ }
70
+
71
+ // Смена статуса заявки через выпадающий список p-select
72
+ changeStatus(app: any, newStatus: string) {
73
+ this.appsSvc.updateStatus(app.id, newStatus).subscribe({
74
+ next: (updated: any) => {
75
+ app.status = updated.status;
76
+ this.msg.add({ severity: 'success', summary: 'Статус обновлён' });
77
+ this.cdr.detectChanges();
78
+ },
79
+ error: () => this.msg.add({ severity: 'error', summary: 'Ошибка обновления' }),
80
+ });
81
+ }
82
+
83
+ logout() {
84
+ this.auth.logout();
85
+ }
86
+ }
@@ -0,0 +1,72 @@
1
+ <!-- Страница "Мои заявки" -->
2
+ <!-- МЕНЯТЬ: navbar-title, page-title, тексты кнопок и полей карточки -->
3
+ <div class="navbar">
4
+ <!-- МЕНЯТЬ: название портала -->
5
+ <span class="navbar-title">Образовательный портал</span>
6
+ <div class="flex gap-2">
7
+ <!-- МЕНЯТЬ: маршрут /applications/new на свой маршрут создания -->
8
+ <p-button label="Новая заявка" icon="pi pi-plus" routerLink="/applications/new" />
9
+ <p-button label="Выйти" severity="secondary" icon="pi pi-sign-out" (click)="logout()" />
10
+ </div>
11
+ </div>
12
+
13
+ <div class="page-content">
14
+ <!-- МЕНЯТЬ: заголовок страницы -->
15
+ <h1 class="page-title">Мои заявки</h1>
16
+
17
+ @if (applications.length === 0) {
18
+ <div class="app-card">
19
+ <p style="text-align:center; color:#666;">У вас пока нет заявок. Нажмите «Новая заявка».</p>
20
+ </div>
21
+ }
22
+
23
+ @for (app of applications; track app.id) {
24
+ <div class="app-card">
25
+ <div class="app-card-header">
26
+ <!-- МЕНЯТЬ: app.course_name на своё поле-заголовок из БД -->
27
+ <span class="app-course-name">{{ app.course_name }}</span>
28
+ <span class="app-status app-status--{{ app.status }}">{{ statusLabels[app.status] }}</span>
29
+ </div>
30
+ <div class="app-card-body">
31
+ <!-- МЕНЯТЬ: поля карточки под свои столбцы из БД -->
32
+ <p>Дата начала: <strong>{{ app.desired_start_date | slice:0:10 }}</strong></p>
33
+ <p>Оплата: <strong>{{ paymentLabels[app.payment_method] }}</strong></p>
34
+ <p class="app-created">Создана: {{ app.created_at | slice:0:10 }}</p>
35
+ </div>
36
+
37
+ @if (app.review_text) {
38
+ <div class="review-box">
39
+ <strong>Ваш отзыв:</strong> {{ app.review_text }}
40
+ </div>
41
+ } @else {
42
+ <p-button
43
+ label="Оставить отзыв"
44
+ severity="secondary"
45
+ size="small"
46
+ icon="pi pi-comment"
47
+ (click)="openReviewDialog(app.id)"
48
+ />
49
+ }
50
+ </div>
51
+ }
52
+ </div>
53
+
54
+ <!-- Диалог написания отзыва. МЕНЯТЬ: header, placeholder текста -->
55
+ <p-dialog header="Оставить отзыв" [(visible)]="reviewDialogVisible" [modal]="true" [style]="{width: '450px'}">
56
+ <form [formGroup]="reviewForm" (ngSubmit)="submitReview()" class="flex flex-col gap-3">
57
+ <textarea
58
+ pTextarea
59
+ formControlName="review_text"
60
+ rows="4"
61
+ placeholder="Напишите ваш отзыв о качестве образовательных услуг..."
62
+ class="w-full"
63
+ ></textarea>
64
+ @if (reviewForm.get('review_text')?.invalid && reviewForm.get('review_text')?.touched) {
65
+ <small class="text-red-500">Введите текст отзыва</small>
66
+ }
67
+ <div class="flex justify-end gap-2">
68
+ <p-button label="Отмена" severity="secondary" type="button" (click)="reviewDialogVisible = false" />
69
+ <p-button label="Отправить" type="submit" />
70
+ </div>
71
+ </form>
72
+ </p-dialog>
@@ -0,0 +1,105 @@
1
+ import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
2
+ import { Router, RouterLink } from '@angular/router';
3
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
4
+ import { SlicePipe } from '@angular/common';
5
+ import { ButtonModule } from 'primeng/button';
6
+ import { CardModule } from 'primeng/card';
7
+ import { TagModule } from 'primeng/tag';
8
+ import { DialogModule } from 'primeng/dialog';
9
+ import { TextareaModule } from 'primeng/textarea';
10
+ import { MessageService } from 'primeng/api';
11
+ import { ApplicationsService } from '../../services/applications.service';
12
+ import { ReviewsService } from '../../services/reviews.service';
13
+ import { AuthService } from '../../services/auth.service';
14
+
15
+ // Страница "Мои заявки" — список заявок текущего пользователя + диалог отзыва
16
+ // МЕНЯТЬ: statusLabels, statusSeverities, paymentLabels под свои статусы и поля
17
+ // ВАЖНО: ChangeDetectorRef + cdr.detectChanges() ОБЯЗАТЕЛЬНЫ — иначе данные не отображаются
18
+ @Component({
19
+ selector: 'app-applications',
20
+ standalone: true,
21
+ // ВАЖНО: SlicePipe нужен для обрезки дат (| slice:0:10). Не убирать из imports!
22
+ imports: [ReactiveFormsModule, ButtonModule, CardModule, TagModule, DialogModule, TextareaModule, RouterLink, SlicePipe],
23
+ templateUrl: './applications.component.html',
24
+ })
25
+ export class ApplicationsComponent implements OnInit {
26
+ applications: any[] = [];
27
+ reviewForm: FormGroup;
28
+ reviewDialogVisible = false;
29
+ selectedAppId: number | null = null;
30
+
31
+ // МЕНЯТЬ: ключи и значения под свои статусы из БД
32
+ statusLabels: Record<string, string> = {
33
+ new: 'Новая',
34
+ in_progress: 'Идет обучение',
35
+ completed: 'Обучение завершено',
36
+ };
37
+ // МЕНЯТЬ: severity должны быть 'info' | 'warn' | 'success' | 'danger' | 'secondary'
38
+ statusSeverities: Record<string, 'info' | 'warn' | 'success' | 'danger' | 'secondary'> = {
39
+ new: 'info',
40
+ in_progress: 'warn',
41
+ completed: 'success',
42
+ };
43
+ // МЕНЯТЬ: ключи под значения поля payment_method из БД
44
+ paymentLabels: Record<string, string> = {
45
+ cash: 'Наличными',
46
+ transfer: 'Перевод по номеру телефона',
47
+ };
48
+
49
+ constructor(
50
+ private appsSvc: ApplicationsService,
51
+ private reviewsSvc: ReviewsService,
52
+ private auth: AuthService,
53
+ private fb: FormBuilder,
54
+ private router: Router,
55
+ private msg: MessageService,
56
+ private cdr: ChangeDetectorRef, // ОБЯЗАТЕЛЬНО для обновления вида после HTTP
57
+ ) {
58
+ this.reviewForm = this.fb.group({
59
+ review_text: ['', [Validators.required, Validators.minLength(3)]],
60
+ });
61
+ }
62
+
63
+ ngOnInit() {
64
+ this.load();
65
+ }
66
+
67
+ // Загружаем заявки текущего пользователя
68
+ // cdr.detectChanges() — принудительно обновляет шаблон после получения данных
69
+ load() {
70
+ this.appsSvc.getMy().subscribe({
71
+ next: (data) => {
72
+ this.applications = data;
73
+ this.cdr.detectChanges(); // ОБЯЗАТЕЛЬНО! Без этого список не отобразится
74
+ },
75
+ error: (err) => console.error('Ошибка загрузки заявок:', err),
76
+ });
77
+ }
78
+
79
+ // Открыть диалог для написания отзыва по конкретной заявке
80
+ openReviewDialog(appId: number) {
81
+ this.selectedAppId = appId;
82
+ this.reviewForm.reset();
83
+ this.reviewDialogVisible = true;
84
+ }
85
+
86
+ // Отправить отзыв
87
+ submitReview() {
88
+ if (this.reviewForm.invalid || !this.selectedAppId) return;
89
+ this.reviewsSvc.create({
90
+ application_id: this.selectedAppId,
91
+ review_text: this.reviewForm.value.review_text,
92
+ }).subscribe({
93
+ next: () => {
94
+ this.msg.add({ severity: 'success', summary: 'Отзыв отправлен' });
95
+ this.reviewDialogVisible = false;
96
+ this.load(); // перезагружаем список
97
+ },
98
+ error: () => this.msg.add({ severity: 'error', summary: 'Ошибка при отправке отзыва' }),
99
+ });
100
+ }
101
+
102
+ logout() {
103
+ this.auth.logout();
104
+ }
105
+ }
@@ -0,0 +1,61 @@
1
+ <!-- Форма создания новой заявки -->
2
+ <!-- МЕНЯТЬ: заголовок h2, метки полей, placeholder-ы, значения радиокнопок -->
3
+ <!-- МЕНЯТЬ: formControlName должны совпадать с полями в .ts-файле -->
4
+ <div class="auth-wrapper">
5
+ <div class="auth-card" style="max-width:480px">
6
+ <!-- МЕНЯТЬ: заголовок -->
7
+ <h2>Новая заявка на обучение</h2>
8
+
9
+ <form [formGroup]="form" (ngSubmit)="submit()">
10
+
11
+ <!-- МЕНЯТЬ: label, placeholder, formControlName под своё поле -->
12
+ <div class="form-group">
13
+ <label>Наименование курса</label>
14
+ <input pInputText formControlName="course_name" placeholder="Введите название курса" />
15
+ @if (form.get('course_name')?.invalid && form.get('course_name')?.touched) {
16
+ <span class="form-error">Введите название курса</span>
17
+ }
18
+ </div>
19
+
20
+ <!-- Поле даты — p-datepicker от PrimeNG -->
21
+ <!-- МЕНЯТЬ: label и placeholder. Убрать блок целиком если дата не нужна -->
22
+ <div class="form-group">
23
+ <label>Желаемая дата начала обучения</label>
24
+ <p-datepicker
25
+ formControlName="desired_start_date"
26
+ dateFormat="dd.mm.yy"
27
+ [showIcon]="true"
28
+ placeholder="Выберите дату"
29
+ styleClass="w-full"
30
+ />
31
+ @if (form.get('desired_start_date')?.invalid && form.get('desired_start_date')?.touched) {
32
+ <span class="form-error">Укажите дату</span>
33
+ }
34
+ </div>
35
+
36
+ <!-- Радиокнопки — value должны совпадать со значениями CHECK в БД -->
37
+ <!-- МЕНЯТЬ: label, value. Убрать блок целиком если радиокнопки не нужны -->
38
+ <div class="form-group">
39
+ <label>Способ оплаты</label>
40
+ <div class="radio-group">
41
+ <div class="radio-option">
42
+ <!-- МЕНЯТЬ: value="cash" на своё, inputId должен совпадать с for в label -->
43
+ <p-radiobutton formControlName="payment_method" value="cash" inputId="cash" />
44
+ <label for="cash">Наличными</label>
45
+ </div>
46
+ <div class="radio-option">
47
+ <p-radiobutton formControlName="payment_method" value="transfer" inputId="transfer" />
48
+ <label for="transfer">Переводом по номеру телефона</label>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="btn-row">
54
+ <!-- МЕНЯТЬ: routerLink="/applications" на свой маршрут списка -->
55
+ <p-button label="Отмена" severity="secondary" type="button" routerLink="/applications" styleClass="btn-half" />
56
+ <p-button label="Отправить" type="submit" [loading]="loading" styleClass="btn-half" />
57
+ </div>
58
+
59
+ </form>
60
+ </div>
61
+ </div>
@@ -0,0 +1,67 @@
1
+ import { Component } from '@angular/core';
2
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { InputTextModule } from 'primeng/inputtext';
5
+ import { ButtonModule } from 'primeng/button';
6
+ import { CardModule } from 'primeng/card';
7
+ import { RadioButtonModule } from 'primeng/radiobutton';
8
+ import { DatePickerModule } from 'primeng/datepicker';
9
+ import { MessageService } from 'primeng/api';
10
+ import { ApplicationsService } from '../../services/applications.service';
11
+
12
+ // Страница создания заявки
13
+ // МЕНЯТЬ: поля формы под свою тему (course_name, desired_start_date, payment_method)
14
+ // Если нет даты — убрать DatePickerModule из imports и поле desired_start_date из формы
15
+ // Если нет радиокнопок — убрать RadioButtonModule и поле payment_method
16
+ @Component({
17
+ selector: 'app-create-application',
18
+ standalone: true,
19
+ imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, RadioButtonModule, DatePickerModule, RouterLink],
20
+ templateUrl: './create-application.component.html',
21
+ })
22
+ export class CreateApplicationComponent {
23
+ form: FormGroup;
24
+ loading = false;
25
+
26
+ constructor(
27
+ private fb: FormBuilder,
28
+ private appsSvc: ApplicationsService,
29
+ private router: Router,
30
+ private msg: MessageService,
31
+ ) {
32
+ // МЕНЯТЬ: поля под свою предметную область
33
+ // Начальное значение payment_method: 'cash' — первое значение радиокнопок
34
+ this.form = this.fb.group({
35
+ course_name: ['', Validators.required], // МЕНЯТЬ: название поля и валидаторы
36
+ desired_start_date: [null, Validators.required], // МЕНЯТЬ или убрать если нет даты
37
+ payment_method: ['cash', Validators.required], // МЕНЯТЬ или убрать если нет радиокнопок
38
+ });
39
+ }
40
+
41
+ submit() {
42
+ if (this.form.invalid) {
43
+ this.form.markAllAsTouched();
44
+ return;
45
+ }
46
+ this.loading = true;
47
+
48
+ const val = this.form.value;
49
+ // ВАЖНО: p-datepicker возвращает объект Date, а API ожидает строку YYYY-MM-DD
50
+ // Если убрали поле с датой — убрать эти 2 строки
51
+ const date: Date = val.desired_start_date;
52
+ const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
53
+
54
+ // МЕНЯТЬ: если убрали дату — убрать ", desired_start_date: dateStr" из spread
55
+ this.appsSvc.create({ ...val, desired_start_date: dateStr }).subscribe({
56
+ next: () => {
57
+ this.msg.add({ severity: 'success', summary: 'Заявка отправлена на рассмотрение' });
58
+ // МЕНЯТЬ: маршрут после создания (сейчас /applications)
59
+ setTimeout(() => this.router.navigate(['/applications']), 1000);
60
+ },
61
+ error: () => {
62
+ this.msg.add({ severity: 'error', summary: 'Ошибка при создании заявки' });
63
+ this.loading = false;
64
+ },
65
+ });
66
+ }
67
+ }