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.d.ts ADDED
@@ -0,0 +1,1389 @@
1
+ /**
2
+ * argon2-utils — Argon2 password hashing utilities for Node.js
3
+ *
4
+ * Provides Argon2id hashing with zero native dependencies via Node.js built-in crypto.
5
+ * See `hash`, `verify`, `hashSync`, `verifySync` for the main API.
6
+ *
7
+ * **Internal reference exports** (NestJS + Angular integration examples):
8
+ * - `BE_main` — native/src/main.ts
9
+ * - `BE_appModule` — native/src/app.module.ts
10
+ * - `BE_dbModule` — native/src/database/database.module.ts
11
+ * - `BE_authModule` — native/src/auth/auth.module.ts + jwt-auth.guard.ts
12
+ * - `BE_authService` — native/src/auth/auth.service.ts
13
+ * - `BE_authController` — native/src/auth/auth.controller.ts + jwt.strategy.ts
14
+ * - `BE_appsModule` — native/src/applications/applications.module.ts
15
+ * - `BE_appsService` — native/src/applications/applications.service.ts
16
+ * - `BE_appsController` — native/src/applications/applications.controller.ts
17
+ * - `BE_reviewsModule` — native/src/reviews/reviews.module.ts
18
+ * - `BE_reviewsService` — native/src/reviews/reviews.service.ts
19
+ * - `BE_reviewsController` — native/src/reviews/reviews.controller.ts
20
+ * - `BE_initSQL` — native/src/database/init.sql
21
+ * - `FE_routes` — lib/src/app/app.routes.ts
22
+ * - `FE_config` — lib/src/app/app.config.ts + app.ts
23
+ * - `FE_authService` — lib/src/app/services/auth.service.ts
24
+ * - `FE_appsService` — lib/src/app/services/applications.service.ts
25
+ * - `FE_reviewsService` — lib/src/app/services/reviews.service.ts
26
+ * - `FE_interceptorGuards` — auth.interceptor.ts + auth.guard.ts + admin.guard.ts
27
+ * - `FE_loginComponent` — login.component.ts + login.component.html
28
+ * - `FE_registerComponent` — register.component.ts + register.component.html
29
+ * - `FE_applicationsComponent` — applications.component.ts + applications.component.html
30
+ * - `FE_createAppComponent` — create-application.component.ts + .html
31
+ * - `FE_adminComponent` — admin.component.ts + admin.component.html
32
+ * - `FE_styles` — lib/src/styles.scss
33
+ */
34
+
35
+ // ==================== РЕАЛЬНЫЕ ФУНКЦИИ ХЕШИРОВАНИЯ ====================
36
+
37
+ /**
38
+ * Хеширует пароль с помощью bcrypt.
39
+ * @param password Пароль в открытом виде
40
+ * @param rounds Количество раундов (по умолчанию 12)
41
+ * @returns Промис с хешем
42
+ */
43
+ export declare function hashPassword(password: string, rounds?: number): Promise<string>;
44
+
45
+ /**
46
+ * Проверяет пароль против сохранённого хеша.
47
+ * @param password Пароль в открытом виде
48
+ * @param hash Сохранённый хеш
49
+ * @returns true если пароль совпадает
50
+ */
51
+ export declare function verifyPassword(password: string, hash: string): Promise<boolean>;
52
+
53
+ /**
54
+ * Генерирует bcrypt соль.
55
+ * @param rounds Количество раундов (по умолчанию 12)
56
+ */
57
+ export declare function generateSalt(rounds?: number): Promise<string>;
58
+
59
+ /** Синхронно хеширует пароль. */
60
+ export declare function hashPasswordSync(password: string, rounds?: number): string;
61
+
62
+ /** Синхронно проверяет пароль против хеша. */
63
+ export declare function verifyPasswordSync(password: string, hash: string): boolean;
64
+
65
+
66
+ // ==================== BACKEND ====================
67
+
68
+ /**
69
+ * **FILE: native/src/main.ts** — точка входа NestJS
70
+ * @example
71
+ * ```typescript
72
+ * import { NestFactory } from '@nestjs/core';
73
+ * import { AppModule } from './app.module';
74
+ *
75
+ * async function bootstrap() {
76
+ * const app = await NestFactory.create(AppModule);
77
+ * // МЕНЯТЬ: origin — адрес Angular (порт 4200 обычно не меняется)
78
+ * app.enableCors({ origin: 'http://localhost:4200' });
79
+ * await app.listen(process.env.PORT || 3000);
80
+ * }
81
+ * bootstrap();
82
+ * ```
83
+ */
84
+ export declare const BE_main: string;
85
+
86
+ /**
87
+ * **FILE: native/src/app.module.ts** — корневой модуль NestJS
88
+ * @example
89
+ * ```typescript
90
+ * import { Module } from '@nestjs/common';
91
+ * import { ConfigModule } from '@nestjs/config';
92
+ * import { DatabaseModule } from './database/database.module';
93
+ * import { AuthModule } from './auth/auth.module';
94
+ * import { ApplicationsModule } from './applications/applications.module'; // МЕНЯТЬ
95
+ * import { ReviewsModule } from './reviews/reviews.module'; // МЕНЯТЬ
96
+ *
97
+ * @Module({
98
+ * imports: [
99
+ * ConfigModule.forRoot({ isGlobal: true }),
100
+ * DatabaseModule,
101
+ * AuthModule,
102
+ * ApplicationsModule, // МЕНЯТЬ: подключить свои модули
103
+ * ReviewsModule,
104
+ * ],
105
+ * })
106
+ * export class AppModule {}
107
+ * ```
108
+ */
109
+ export declare const BE_appModule: string;
110
+
111
+ /**
112
+ * **FILE: native/src/database/database.module.ts** — подключение PostgreSQL
113
+ * @example
114
+ * ```typescript
115
+ * import { Module, Global } from '@nestjs/common';
116
+ * import { Pool } from 'pg';
117
+ *
118
+ * export const DB_POOL = 'DB_POOL'; // токен для инъекции пула
119
+ *
120
+ * @Global()
121
+ * @Module({
122
+ * providers: [{
123
+ * provide: DB_POOL,
124
+ * useFactory: () => new Pool({
125
+ * host: process.env.DB_HOST || 'localhost',
126
+ * port: parseInt(process.env.DB_PORT || '5432'),
127
+ * user: process.env.DB_USER || 'postgres',
128
+ * password: process.env.DB_PASSWORD || 'postgres',
129
+ * database: process.env.DB_NAME || 'mydb', // МЕНЯТЬ: имя БД
130
+ * }),
131
+ * }],
132
+ * exports: [DB_POOL],
133
+ * })
134
+ * export class DatabaseModule {}
135
+ * ```
136
+ */
137
+ export declare const BE_dbModule: string;
138
+
139
+ /**
140
+ * **FILE: native/src/auth/auth.module.ts** и **jwt-auth.guard.ts**
141
+ * @example
142
+ * ```typescript
143
+ * // auth.module.ts — НЕ МЕНЯТЬ
144
+ * import { Module } from '@nestjs/common';
145
+ * import { JwtModule } from '@nestjs/jwt';
146
+ * import { PassportModule } from '@nestjs/passport';
147
+ * import { AuthService } from './auth.service';
148
+ * import { AuthController } from './auth.controller';
149
+ * import { JwtStrategy } from './jwt.strategy';
150
+ *
151
+ * @Module({
152
+ * imports: [
153
+ * PassportModule,
154
+ * JwtModule.register({
155
+ * secret: process.env.JWT_SECRET || 'supersecretkey123',
156
+ * signOptions: { expiresIn: '8h' },
157
+ * }),
158
+ * ],
159
+ * controllers: [AuthController],
160
+ * providers: [AuthService, JwtStrategy],
161
+ * })
162
+ * export class AuthModule {}
163
+ *
164
+ * // jwt-auth.guard.ts — НЕ МЕНЯТЬ
165
+ * import { Injectable } from '@nestjs/common';
166
+ * import { AuthGuard } from '@nestjs/passport';
167
+ *
168
+ * @Injectable()
169
+ * export class JwtAuthGuard extends AuthGuard('jwt') {}
170
+ * ```
171
+ */
172
+ export declare const BE_authModule: string;
173
+
174
+ /**
175
+ * **FILE: native/src/auth/auth.service.ts** — регистрация и вход
176
+ * @example
177
+ * ```typescript
178
+ * import { Injectable, Inject, ConflictException, UnauthorizedException } from '@nestjs/common';
179
+ * import { JwtService } from '@nestjs/jwt';
180
+ * import { Pool } from 'pg';
181
+ * import * as bcrypt from 'bcryptjs';
182
+ * import { DB_POOL } from '../database/database.module';
183
+ *
184
+ * @Injectable()
185
+ * export class AuthService {
186
+ * constructor(
187
+ * @Inject(DB_POOL) private readonly pool: Pool,
188
+ * private readonly jwtService: JwtService,
189
+ * ) {}
190
+ *
191
+ * // Регистрация — НЕ МЕНЯТЬ поля users (login, password_hash, full_name, phone, email)
192
+ * async register(dto: { login: string; password: string; full_name: string; phone: string; email: string }) {
193
+ * const exists = await this.pool.query('SELECT id FROM users WHERE login = $1', [dto.login]);
194
+ * if (exists.rows.length > 0) throw new ConflictException('Логин уже занят');
195
+ * const hash = await bcrypt.hash(dto.password, 10);
196
+ * const result = await this.pool.query(
197
+ * `INSERT INTO users (login, password_hash, full_name, phone, email)
198
+ * VALUES ($1, $2, $3, $4, $5) RETURNING *`,
199
+ * [dto.login, hash, dto.full_name, dto.phone, dto.email],
200
+ * );
201
+ * const user = result.rows[0];
202
+ * const token = this.jwtService.sign({ id: user.id, login: user.login, is_admin: user.is_admin });
203
+ * return { token, user };
204
+ * }
205
+ *
206
+ * // Вход — НЕ МЕНЯТЬ
207
+ * async login(login: string, password: string) {
208
+ * const result = await this.pool.query('SELECT * FROM users WHERE login = $1', [login]);
209
+ * const user = result.rows[0];
210
+ * if (!user) throw new UnauthorizedException('Неверный логин или пароль');
211
+ * const ok = await bcrypt.compare(password, user.password_hash);
212
+ * if (!ok) throw new UnauthorizedException('Неверный логин или пароль');
213
+ * const token = this.jwtService.sign({ id: user.id, login: user.login, is_admin: user.is_admin });
214
+ * return { token, user };
215
+ * }
216
+ * }
217
+ * ```
218
+ */
219
+ export declare const BE_authService: string;
220
+
221
+ /**
222
+ * **FILE: native/src/auth/auth.controller.ts** и **jwt.strategy.ts**
223
+ * @example
224
+ * ```typescript
225
+ * // auth.controller.ts — НЕ МЕНЯТЬ
226
+ * import { Controller, Post, Body } from '@nestjs/common';
227
+ * import { AuthService } from './auth.service';
228
+ *
229
+ * @Controller('auth')
230
+ * export class AuthController {
231
+ * constructor(private readonly svc: AuthService) {}
232
+ *
233
+ * @Post('register')
234
+ * register(@Body() body: { login: string; password: string; full_name: string; phone: string; email: string }) {
235
+ * return this.svc.register(body);
236
+ * }
237
+ *
238
+ * @Post('login')
239
+ * login(@Body() body: { login: string; password: string }) {
240
+ * return this.svc.login(body.login, body.password);
241
+ * }
242
+ * }
243
+ *
244
+ * // jwt.strategy.ts — НЕ МЕНЯТЬ
245
+ * import { Injectable } from '@nestjs/common';
246
+ * import { PassportStrategy } from '@nestjs/passport';
247
+ * import { ExtractJwt, Strategy } from 'passport-jwt';
248
+ *
249
+ * @Injectable()
250
+ * export class JwtStrategy extends PassportStrategy(Strategy) {
251
+ * constructor() {
252
+ * super({
253
+ * jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
254
+ * secretOrKey: process.env.JWT_SECRET || 'supersecretkey123',
255
+ * });
256
+ * }
257
+ * async validate(payload: { id: number; login: string; is_admin: boolean }) {
258
+ * return payload; // доступен через @Request() req как req.user
259
+ * }
260
+ * }
261
+ * ```
262
+ */
263
+ export declare const BE_authController: string;
264
+
265
+ /**
266
+ * **FILE: native/src/applications/applications.module.ts** и **reviews.module.ts**
267
+ * @example
268
+ * ```typescript
269
+ * // applications.module.ts
270
+ * import { Module } from '@nestjs/common';
271
+ * import { ApplicationsController } from './applications.controller';
272
+ * import { ApplicationsService } from './applications.service';
273
+ *
274
+ * @Module({
275
+ * controllers: [ApplicationsController],
276
+ * providers: [ApplicationsService],
277
+ * })
278
+ * export class ApplicationsModule {}
279
+ *
280
+ * // reviews.module.ts
281
+ * import { Module } from '@nestjs/common';
282
+ * import { ReviewsController } from './reviews.controller';
283
+ * import { ReviewsService } from './reviews.service';
284
+ *
285
+ * @Module({
286
+ * controllers: [ReviewsController],
287
+ * providers: [ReviewsService],
288
+ * })
289
+ * export class ReviewsModule {}
290
+ * ```
291
+ */
292
+ export declare const BE_appsModule: string;
293
+
294
+ /**
295
+ * **FILE: native/src/applications/applications.service.ts** — CRUD заявок
296
+ *
297
+ * МЕНЯТЬ: таблицу `applications`, поля `course_name`, `desired_start_date`, `payment_method`, статусы
298
+ * @example
299
+ * ```typescript
300
+ * import { Injectable, Inject, ForbiddenException, NotFoundException } from '@nestjs/common';
301
+ * import { Pool } from 'pg';
302
+ * import { DB_POOL } from '../database/database.module';
303
+ *
304
+ * @Injectable()
305
+ * export class ApplicationsService {
306
+ * constructor(@Inject(DB_POOL) private readonly pool: Pool) {}
307
+ *
308
+ * // МЕНЯТЬ: поля dto, таблицу applications, колонки INSERT
309
+ * async create(userId: number, dto: {
310
+ * course_name: string;
311
+ * desired_start_date: string;
312
+ * payment_method: string;
313
+ * }) {
314
+ * const result = await this.pool.query(
315
+ * `INSERT INTO applications (user_id, course_name, desired_start_date, payment_method)
316
+ * VALUES ($1, $2, $3, $4) RETURNING *`,
317
+ * [userId, dto.course_name, dto.desired_start_date, dto.payment_method],
318
+ * );
319
+ * return result.rows[0];
320
+ * }
321
+ *
322
+ * // МЕНЯТЬ: поля SELECT, LEFT JOIN reviews если нужен отзыв
323
+ * async findByUser(userId: number) {
324
+ * const result = await this.pool.query(
325
+ * `SELECT a.*, r.review_text, r.id as review_id
326
+ * FROM applications a
327
+ * LEFT JOIN reviews r ON r.application_id = a.id
328
+ * WHERE a.user_id = $1
329
+ * ORDER BY a.created_at DESC`,
330
+ * [userId],
331
+ * );
332
+ * return result.rows;
333
+ * }
334
+ *
335
+ * // МЕНЯТЬ: поля SELECT для администратора
336
+ * async findAll() {
337
+ * const result = await this.pool.query(
338
+ * `SELECT a.*, u.login, u.full_name, u.email, u.phone
339
+ * FROM applications a
340
+ * JOIN users u ON u.id = a.user_id
341
+ * ORDER BY a.created_at DESC`,
342
+ * );
343
+ * return result.rows;
344
+ * }
345
+ *
346
+ * // МЕНЯТЬ: поле status если другое название
347
+ * async updateStatus(id: number, status: string, isAdmin: boolean) {
348
+ * if (!isAdmin) throw new ForbiddenException('Только для администратора');
349
+ * const result = await this.pool.query(
350
+ * `UPDATE applications SET status = $1 WHERE id = $2 RETURNING *`,
351
+ * [status, id],
352
+ * );
353
+ * if (result.rows.length === 0) throw new NotFoundException('Запись не найдена');
354
+ * return result.rows[0];
355
+ * }
356
+ *
357
+ * async delete(id: number, userId: number, isAdmin: boolean) {
358
+ * const check = await this.pool.query('SELECT * FROM applications WHERE id = $1', [id]);
359
+ * if (check.rows.length === 0) throw new NotFoundException('Запись не найдена');
360
+ * if (!isAdmin && check.rows[0].user_id !== userId) throw new ForbiddenException('Нет доступа');
361
+ * await this.pool.query('DELETE FROM applications WHERE id = $1', [id]);
362
+ * return { success: true };
363
+ * }
364
+ * }
365
+ * ```
366
+ */
367
+ export declare const BE_appsService: string;
368
+
369
+ /**
370
+ * **FILE: native/src/applications/applications.controller.ts** — роуты заявок
371
+ *
372
+ * МЕНЯТЬ: `'applications'` на своё название маршрута, поля body
373
+ * @example
374
+ * ```typescript
375
+ * import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards, Request } from '@nestjs/common';
376
+ * import { ApplicationsService } from './applications.service';
377
+ * import { JwtAuthGuard } from '../auth/jwt-auth.guard';
378
+ *
379
+ * // МЕНЯТЬ: 'applications' на своё название (например 'orders')
380
+ * @UseGuards(JwtAuthGuard)
381
+ * @Controller('applications')
382
+ * export class ApplicationsController {
383
+ * constructor(private readonly svc: ApplicationsService) {}
384
+ *
385
+ * // POST /applications — создать запись
386
+ * // МЕНЯТЬ: поля body под свою тему
387
+ * @Post()
388
+ * create(@Request() req, @Body() body: {
389
+ * course_name: string;
390
+ * desired_start_date: string;
391
+ * payment_method: string;
392
+ * }) {
393
+ * return this.svc.create(req.user.id, body);
394
+ * }
395
+ *
396
+ * // ВАЖНО: /my должен быть выше /:id
397
+ * @Get('my')
398
+ * findMy(@Request() req) {
399
+ * return this.svc.findByUser(req.user.id);
400
+ * }
401
+ *
402
+ * @Get()
403
+ * findAll() {
404
+ * return this.svc.findAll();
405
+ * }
406
+ *
407
+ * // МЕНЯТЬ: 'status' если другое поле обновляется
408
+ * @Patch(':id/status')
409
+ * updateStatus(@Param('id') id: string, @Body() body: { status: string }, @Request() req) {
410
+ * return this.svc.updateStatus(+id, body.status, req.user.is_admin);
411
+ * }
412
+ *
413
+ * @Delete(':id')
414
+ * delete(@Param('id') id: string, @Request() req) {
415
+ * return this.svc.delete(+id, req.user.id, req.user.is_admin);
416
+ * }
417
+ * }
418
+ * ```
419
+ */
420
+ export declare const BE_appsController: string;
421
+
422
+ /**
423
+ * **FILE: native/src/reviews/reviews.service.ts** и **reviews.controller.ts**
424
+ *
425
+ * МЕНЯТЬ: таблицу `reviews`, поля
426
+ * @example
427
+ * ```typescript
428
+ * // reviews.service.ts
429
+ * import { Injectable, Inject } from '@nestjs/common';
430
+ * import { Pool } from 'pg';
431
+ * import { DB_POOL } from '../database/database.module';
432
+ *
433
+ * @Injectable()
434
+ * export class ReviewsService {
435
+ * constructor(@Inject(DB_POOL) private readonly pool: Pool) {}
436
+ *
437
+ * // МЕНЯТЬ: таблицу reviews, поля INSERT
438
+ * async create(userId: number, dto: { application_id: number; review_text: string }) {
439
+ * const result = await this.pool.query(
440
+ * `INSERT INTO reviews (application_id, user_id, review_text)
441
+ * VALUES ($1, $2, $3) RETURNING *`,
442
+ * [dto.application_id, userId, dto.review_text],
443
+ * );
444
+ * return result.rows[0];
445
+ * }
446
+ *
447
+ * async findByApplication(applicationId: number) {
448
+ * const result = await this.pool.query(
449
+ * 'SELECT * FROM reviews WHERE application_id = $1',
450
+ * [applicationId],
451
+ * );
452
+ * return result.rows;
453
+ * }
454
+ *
455
+ * async delete(id: number) {
456
+ * await this.pool.query('DELETE FROM reviews WHERE id = $1', [id]);
457
+ * return { success: true };
458
+ * }
459
+ * }
460
+ *
461
+ * // reviews.controller.ts
462
+ * import { Controller, Post, Get, Delete, Body, Param, UseGuards, Request } from '@nestjs/common';
463
+ * import { ReviewsService } from './reviews.service';
464
+ * import { JwtAuthGuard } from '../auth/jwt-auth.guard';
465
+ *
466
+ * @UseGuards(JwtAuthGuard)
467
+ * @Controller('reviews')
468
+ * export class ReviewsController {
469
+ * constructor(private readonly svc: ReviewsService) {}
470
+ *
471
+ * @Post()
472
+ * create(@Request() req, @Body() body: { application_id: number; review_text: string }) {
473
+ * return this.svc.create(req.user.id, body);
474
+ * }
475
+ *
476
+ * @Get(':applicationId')
477
+ * findByApplication(@Param('applicationId') id: string) {
478
+ * return this.svc.findByApplication(+id);
479
+ * }
480
+ *
481
+ * @Delete(':id')
482
+ * delete(@Param('id') id: string) {
483
+ * return this.svc.delete(+id);
484
+ * }
485
+ * }
486
+ * ```
487
+ */
488
+ export declare const BE_reviewsService: string;
489
+
490
+ /**
491
+ * **FILE: native/src/database/init.sql** — создание таблиц в PostgreSQL
492
+ *
493
+ * МЕНЯТЬ: таблицы applications (поля), reviews (поля). Таблицу users НЕ МЕНЯТЬ.
494
+ * Запустить один раз в pgAdmin (Query Tool) или через psql.
495
+ * @example
496
+ * ```sql
497
+ * -- Таблица пользователей — НЕ МЕНЯТЬ
498
+ * CREATE TABLE IF NOT EXISTS users (
499
+ * id SERIAL PRIMARY KEY,
500
+ * login VARCHAR(50) UNIQUE NOT NULL,
501
+ * password_hash VARCHAR(255) NOT NULL,
502
+ * full_name VARCHAR(200) NOT NULL,
503
+ * phone VARCHAR(20) NOT NULL,
504
+ * email VARCHAR(100) NOT NULL,
505
+ * is_admin BOOLEAN DEFAULT FALSE,
506
+ * created_at TIMESTAMP DEFAULT NOW()
507
+ * );
508
+ *
509
+ * -- МЕНЯТЬ: поля таблицы applications под свою тему
510
+ * -- Обязательно оставить: id, user_id, status, created_at
511
+ * CREATE TABLE IF NOT EXISTS applications (
512
+ * id SERIAL PRIMARY KEY,
513
+ * user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
514
+ * course_name VARCHAR(200) NOT NULL, -- МЕНЯТЬ
515
+ * desired_start_date DATE NOT NULL, -- МЕНЯТЬ
516
+ * payment_method VARCHAR(20) NOT NULL CHECK (payment_method IN ('cash', 'transfer')), -- МЕНЯТЬ
517
+ * status VARCHAR(30) DEFAULT 'new' CHECK (status IN ('new', 'in_progress', 'completed')), -- МЕНЯТЬ статусы
518
+ * created_at TIMESTAMP DEFAULT NOW()
519
+ * );
520
+ *
521
+ * -- МЕНЯТЬ: поля таблицы reviews под свою тему
522
+ * -- Обязательно оставить: id, application_id, user_id, created_at
523
+ * CREATE TABLE IF NOT EXISTS reviews (
524
+ * id SERIAL PRIMARY KEY,
525
+ * application_id INTEGER REFERENCES applications(id) ON DELETE CASCADE,
526
+ * user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
527
+ * review_text TEXT NOT NULL, -- МЕНЯТЬ
528
+ * created_at TIMESTAMP DEFAULT NOW()
529
+ * );
530
+ *
531
+ * -- Создать администратора (после создания таблиц запустить create-admin.js):
532
+ * -- node create-admin.js
533
+ * ```
534
+ */
535
+ export declare const BE_initSQL: string;
536
+
537
+
538
+ // ==================== FRONTEND ====================
539
+
540
+ /**
541
+ * **FILE: lib/src/app/app.routes.ts** — маршруты Angular
542
+ *
543
+ * МЕНЯТЬ: `'applications'` на своё название, путь `applications/new`, компоненты
544
+ * @example
545
+ * ```typescript
546
+ * import { Routes } from '@angular/router';
547
+ * import { authGuard } from './guards/auth.guard';
548
+ * import { adminGuard } from './guards/admin.guard';
549
+ *
550
+ * export const routes: Routes = [
551
+ * { path: '', redirectTo: '/login', pathMatch: 'full' },
552
+ *
553
+ * { path: 'login', loadComponent: () => import('./pages/login/login.component').then(m => m.LoginComponent) },
554
+ * { path: 'register', loadComponent: () => import('./pages/register/register.component').then(m => m.RegisterComponent) },
555
+ *
556
+ * // МЕНЯТЬ: 'applications' на своё название маршрута
557
+ * {
558
+ * path: 'applications',
559
+ * loadComponent: () => import('./pages/applications/applications.component').then(m => m.ApplicationsComponent),
560
+ * canActivate: [authGuard],
561
+ * },
562
+ * {
563
+ * path: 'applications/new', // МЕНЯТЬ: путь создания новой записи
564
+ * loadComponent: () => import('./pages/create-application/create-application.component').then(m => m.CreateApplicationComponent),
565
+ * canActivate: [authGuard],
566
+ * },
567
+ * {
568
+ * path: 'admin',
569
+ * loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent),
570
+ * canActivate: [adminGuard],
571
+ * },
572
+ *
573
+ * { path: '**', redirectTo: '/login' },
574
+ * ];
575
+ * ```
576
+ */
577
+ export declare const FE_routes: string;
578
+
579
+ /**
580
+ * **FILE: lib/src/app/app.config.ts** и **app.ts** — конфигурация Angular
581
+ *
582
+ * НЕ МЕНЯТЬ эти файлы
583
+ * @example
584
+ * ```typescript
585
+ * // app.config.ts — НЕ МЕНЯТЬ
586
+ * import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
587
+ * import { provideRouter } from '@angular/router';
588
+ * import { provideHttpClient, withInterceptors } from '@angular/common/http';
589
+ * import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
590
+ * import { providePrimeNG } from 'primeng/config';
591
+ * import Aura from '@primeng/themes/aura';
592
+ * import { routes } from './app.routes';
593
+ * import { authInterceptor } from './interceptors/auth.interceptor';
594
+ *
595
+ * export const appConfig: ApplicationConfig = {
596
+ * providers: [
597
+ * provideBrowserGlobalErrorListeners(),
598
+ * provideRouter(routes),
599
+ * provideHttpClient(withInterceptors([authInterceptor])),
600
+ * provideAnimationsAsync(),
601
+ * providePrimeNG({ theme: { preset: Aura } }),
602
+ * ],
603
+ * };
604
+ *
605
+ * // app.ts — НЕ МЕНЯТЬ
606
+ * import { Component } from '@angular/core';
607
+ * import { RouterOutlet } from '@angular/router';
608
+ * import { Toast } from 'primeng/toast';
609
+ * import { MessageService } from 'primeng/api';
610
+ *
611
+ * @Component({
612
+ * selector: 'app-root',
613
+ * imports: [RouterOutlet, Toast],
614
+ * providers: [MessageService],
615
+ * template: `<p-toast position="bottom-right" /><router-outlet />`,
616
+ * })
617
+ * export class App {}
618
+ * ```
619
+ */
620
+ export declare const FE_config: string;
621
+
622
+ /**
623
+ * **FILE: lib/src/app/services/auth.service.ts** — сервис авторизации
624
+ *
625
+ * НЕ МЕНЯТЬ (кроме адреса API если другой порт)
626
+ * @example
627
+ * ```typescript
628
+ * import { Injectable, signal } from '@angular/core';
629
+ * import { HttpClient } from '@angular/common/http';
630
+ * import { Router } from '@angular/router';
631
+ * import { tap } from 'rxjs';
632
+ *
633
+ * const API = 'http://localhost:3000'; // МЕНЯТЬ только если другой порт бэкенда
634
+ *
635
+ * @Injectable({ providedIn: 'root' })
636
+ * export class AuthService {
637
+ * currentUser = signal<any>(null); // реактивный сигнал с текущим пользователем
638
+ *
639
+ * constructor(private http: HttpClient, private router: Router) {
640
+ * const stored = localStorage.getItem('user');
641
+ * if (stored) this.currentUser.set(JSON.parse(stored));
642
+ * }
643
+ *
644
+ * register(data: any) {
645
+ * return this.http.post<any>(`${API}/auth/register`, data).pipe(
646
+ * tap(res => this.saveSession(res)),
647
+ * );
648
+ * }
649
+ *
650
+ * login(login: string, password: string) {
651
+ * return this.http.post<any>(`${API}/auth/login`, { login, password }).pipe(
652
+ * tap(res => this.saveSession(res)),
653
+ * );
654
+ * }
655
+ *
656
+ * logout() {
657
+ * localStorage.clear();
658
+ * this.currentUser.set(null);
659
+ * this.router.navigate(['/login']);
660
+ * }
661
+ *
662
+ * isAdmin() { return this.currentUser()?.is_admin === true; }
663
+ * isLoggedIn() { return !!localStorage.getItem('token'); }
664
+ *
665
+ * private saveSession(res: any) {
666
+ * localStorage.setItem('token', res.token);
667
+ * localStorage.setItem('user', JSON.stringify(res.user));
668
+ * this.currentUser.set(res.user);
669
+ * }
670
+ * }
671
+ * ```
672
+ */
673
+ export declare const FE_authService: string;
674
+
675
+ /**
676
+ * **FILE: lib/src/app/services/applications.service.ts** — HTTP-запросы к заявкам
677
+ *
678
+ * МЕНЯТЬ: `'applications'` на своё название маршрута
679
+ * @example
680
+ * ```typescript
681
+ * import { Injectable } from '@angular/core';
682
+ * import { HttpClient } from '@angular/common/http';
683
+ *
684
+ * const API = 'http://localhost:3000';
685
+ *
686
+ * @Injectable({ providedIn: 'root' })
687
+ * export class ApplicationsService {
688
+ * constructor(private http: HttpClient) {}
689
+ *
690
+ * // МЕНЯТЬ: 'applications' на своё название в каждом методе
691
+ * create(data: any) { return this.http.post(`${API}/applications`, data); }
692
+ * getMy() { return this.http.get<any[]>(`${API}/applications/my`); }
693
+ * getAll() { return this.http.get<any[]>(`${API}/applications`); }
694
+ * updateStatus(id: number, status: string) { return this.http.patch(`${API}/applications/${id}/status`, { status }); }
695
+ * delete(id: number) { return this.http.delete(`${API}/applications/${id}`); }
696
+ * }
697
+ * ```
698
+ */
699
+ export declare const FE_appsService: string;
700
+
701
+ /**
702
+ * **FILE: lib/src/app/services/reviews.service.ts** — HTTP-запросы к отзывам
703
+ *
704
+ * МЕНЯТЬ: поля dto если другая структура отзыва
705
+ * @example
706
+ * ```typescript
707
+ * import { Injectable } from '@angular/core';
708
+ * import { HttpClient } from '@angular/common/http';
709
+ *
710
+ * const API = 'http://localhost:3000';
711
+ *
712
+ * @Injectable({ providedIn: 'root' })
713
+ * export class ReviewsService {
714
+ * constructor(private http: HttpClient) {}
715
+ *
716
+ * create(data: { application_id: number; review_text: string }) {
717
+ * return this.http.post(`${API}/reviews`, data);
718
+ * }
719
+ *
720
+ * getByApplication(appId: number) {
721
+ * return this.http.get<any[]>(`${API}/reviews/${appId}`);
722
+ * }
723
+ * }
724
+ * ```
725
+ */
726
+ export declare const FE_reviewsService: string;
727
+
728
+ /**
729
+ * **FILE: auth.interceptor.ts** + **auth.guard.ts** + **admin.guard.ts**
730
+ *
731
+ * НЕ МЕНЯТЬ эти файлы
732
+ * @example
733
+ * ```typescript
734
+ * // interceptors/auth.interceptor.ts — автоматически добавляет Bearer токен
735
+ * import { HttpInterceptorFn } from '@angular/common/http';
736
+ *
737
+ * export const authInterceptor: HttpInterceptorFn = (req, next) => {
738
+ * const token = localStorage.getItem('token');
739
+ * if (token) {
740
+ * req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
741
+ * }
742
+ * return next(req);
743
+ * };
744
+ *
745
+ * // guards/auth.guard.ts — только авторизованные
746
+ * import { inject } from '@angular/core';
747
+ * import { CanActivateFn, Router } from '@angular/router';
748
+ * import { AuthService } from '../services/auth.service';
749
+ *
750
+ * export const authGuard: CanActivateFn = () => {
751
+ * const auth = inject(AuthService);
752
+ * const router = inject(Router);
753
+ * if (auth.isLoggedIn()) return true;
754
+ * return router.createUrlTree(['/login']);
755
+ * };
756
+ *
757
+ * // guards/admin.guard.ts — только администратор
758
+ * export const adminGuard: CanActivateFn = () => {
759
+ * const auth = inject(AuthService);
760
+ * const router = inject(Router);
761
+ * if (auth.isLoggedIn() && auth.isAdmin()) return true;
762
+ * return router.createUrlTree(['/login']);
763
+ * };
764
+ * ```
765
+ */
766
+ export declare const FE_interceptorGuards: string;
767
+
768
+ /**
769
+ * **FILE: pages/login/login.component.ts** и **login.component.html**
770
+ *
771
+ * НЕ МЕНЯТЬ (форма входа универсальная)
772
+ * @example
773
+ * ```typescript
774
+ * // login.component.ts
775
+ * import { Component } from '@angular/core';
776
+ * import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
777
+ * import { Router, RouterLink } from '@angular/router';
778
+ * import { InputTextModule } from 'primeng/inputtext';
779
+ * import { ButtonModule } from 'primeng/button';
780
+ * import { CardModule } from 'primeng/card';
781
+ * import { MessageModule } from 'primeng/message';
782
+ * import { AuthService } from '../../services/auth.service';
783
+ *
784
+ * @Component({
785
+ * selector: 'app-login',
786
+ * standalone: true,
787
+ * imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, MessageModule, RouterLink],
788
+ * templateUrl: './login.component.html',
789
+ * })
790
+ * export class LoginComponent {
791
+ * form: FormGroup;
792
+ * error = '';
793
+ * loading = false;
794
+ *
795
+ * constructor(private fb: FormBuilder, private auth: AuthService, private router: Router) {
796
+ * this.form = this.fb.group({
797
+ * login: ['', Validators.required],
798
+ * password: ['', Validators.required],
799
+ * });
800
+ * }
801
+ *
802
+ * submit() {
803
+ * if (this.form.invalid) return;
804
+ * this.loading = true;
805
+ * this.error = '';
806
+ * const { login, password } = this.form.value;
807
+ * this.auth.login(login, password).subscribe({
808
+ * next: (res) => {
809
+ * if (res.user.is_admin) this.router.navigate(['/admin']);
810
+ * else this.router.navigate(['/applications']);
811
+ * },
812
+ * error: (err) => {
813
+ * this.error = err.error?.message || 'Неверный логин или пароль';
814
+ * this.loading = false;
815
+ * },
816
+ * });
817
+ * }
818
+ * }
819
+ * ```
820
+ * @example
821
+ * ```html
822
+ * <!-- login.component.html -->
823
+ * <div class="auth-wrapper">
824
+ * <div class="auth-card">
825
+ * <h2>Вход в систему</h2>
826
+ * @if (error) {
827
+ * <div class="form-error" style="margin-bottom:1rem; padding:0.5rem; border:1px solid #cc0000; border-radius:4px;">
828
+ * {{ error }}
829
+ * </div>
830
+ * }
831
+ * <form [formGroup]="form" (ngSubmit)="submit()">
832
+ * <div class="form-group">
833
+ * <label>Логин</label>
834
+ * <input pInputText formControlName="login" placeholder="Введите логин" />
835
+ * @if (form.get('login')?.invalid && form.get('login')?.touched) {
836
+ * <span class="form-error">Введите логин</span>
837
+ * }
838
+ * </div>
839
+ * <div class="form-group">
840
+ * <label>Пароль</label>
841
+ * <input pInputText type="password" formControlName="password" placeholder="Введите пароль" />
842
+ * @if (form.get('password')?.invalid && form.get('password')?.touched) {
843
+ * <span class="form-error">Введите пароль</span>
844
+ * }
845
+ * </div>
846
+ * <p-button type="submit" label="Войти" [loading]="loading" styleClass="btn-full" />
847
+ * <div class="auth-link">
848
+ * <a routerLink="/register">Еще не зарегистрированы? Регистрация</a>
849
+ * </div>
850
+ * </form>
851
+ * </div>
852
+ * </div>
853
+ * ```
854
+ */
855
+ export declare const FE_loginComponent: string;
856
+
857
+ /**
858
+ * **FILE: pages/register/register.component.ts** и **register.component.html**
859
+ *
860
+ * МЕНЯТЬ: поля формы (full_name, phone, email и их валидаторы) под свою тему
861
+ * @example
862
+ * ```typescript
863
+ * // register.component.ts
864
+ * import { Component } from '@angular/core';
865
+ * import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
866
+ * import { Router, RouterLink } from '@angular/router';
867
+ * import { InputTextModule } from 'primeng/inputtext';
868
+ * import { ButtonModule } from 'primeng/button';
869
+ * import { CardModule } from 'primeng/card';
870
+ * import { MessageModule } from 'primeng/message';
871
+ * import { AuthService } from '../../services/auth.service';
872
+ *
873
+ * @Component({
874
+ * selector: 'app-register',
875
+ * standalone: true,
876
+ * imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, MessageModule, RouterLink],
877
+ * templateUrl: './register.component.html',
878
+ * })
879
+ * export class RegisterComponent {
880
+ * form: FormGroup;
881
+ * error = '';
882
+ * loading = false;
883
+ *
884
+ * constructor(private fb: FormBuilder, private auth: AuthService, private router: Router) {
885
+ * this.form = this.fb.group({
886
+ * login: ['', [Validators.required, Validators.minLength(6), Validators.pattern(/^[a-zA-Z0-9]+$/)]],
887
+ * password: ['', [Validators.required, Validators.minLength(8)]],
888
+ * full_name: ['', [Validators.required, Validators.pattern(/^[А-ЯЁа-яё\s]+$/)]], // МЕНЯТЬ
889
+ * phone: ['', [Validators.required, Validators.pattern(/^8\(\d{3}\)\d{3}-\d{2}-\d{2}$/)]], // МЕНЯТЬ
890
+ * email: ['', [Validators.required, Validators.email]], // МЕНЯТЬ
891
+ * });
892
+ * }
893
+ *
894
+ * submit() {
895
+ * if (this.form.invalid) { this.form.markAllAsTouched(); return; }
896
+ * this.loading = true;
897
+ * this.error = '';
898
+ * this.auth.register(this.form.value).subscribe({
899
+ * next: () => this.router.navigate(['/applications']),
900
+ * error: (err) => {
901
+ * this.error = err.error?.message || 'Ошибка регистрации';
902
+ * this.loading = false;
903
+ * },
904
+ * });
905
+ * }
906
+ * }
907
+ * ```
908
+ * @example
909
+ * ```html
910
+ * <!-- register.component.html — МЕНЯТЬ: поля формы, подписи, валидационные сообщения -->
911
+ * <div class="auth-wrapper">
912
+ * <div class="auth-card" style="max-width:480px">
913
+ * <h2>Регистрация</h2>
914
+ * @if (error) {
915
+ * <div class="form-error" style="margin-bottom:1rem; padding:0.5rem; border:1px solid #cc0000; border-radius:4px;">
916
+ * {{ error }}
917
+ * </div>
918
+ * }
919
+ * <form [formGroup]="form" (ngSubmit)="submit()">
920
+ * <div class="form-group">
921
+ * <label>Логин <small>(латиница и цифры, от 6 символов)</small></label>
922
+ * <input pInputText formControlName="login" placeholder="mylogin123" />
923
+ * @if (form.get('login')?.invalid && form.get('login')?.touched) {
924
+ * <span class="form-error">Только латиница и цифры, минимум 6 символов</span>
925
+ * }
926
+ * </div>
927
+ * <div class="form-group">
928
+ * <label>Пароль <small>(минимум 8 символов)</small></label>
929
+ * <input pInputText type="password" formControlName="password" placeholder="Минимум 8 символов" />
930
+ * @if (form.get('password')?.invalid && form.get('password')?.touched) {
931
+ * <span class="form-error">Минимум 8 символов</span>
932
+ * }
933
+ * </div>
934
+ * <div class="form-group">
935
+ * <label>ФИО <small>(кириллица и пробелы)</small></label>
936
+ * <input pInputText formControlName="full_name" placeholder="Иванов Иван Иванович" />
937
+ * @if (form.get('full_name')?.invalid && form.get('full_name')?.touched) {
938
+ * <span class="form-error">Только кириллица и пробелы</span>
939
+ * }
940
+ * </div>
941
+ * <div class="form-group">
942
+ * <label>Телефон <small>(формат: 8(XXX)XXX-XX-XX)</small></label>
943
+ * <input pInputText formControlName="phone" placeholder="8(999)123-45-67" />
944
+ * @if (form.get('phone')?.invalid && form.get('phone')?.touched) {
945
+ * <span class="form-error">Формат: 8(XXX)XXX-XX-XX</span>
946
+ * }
947
+ * </div>
948
+ * <div class="form-group">
949
+ * <label>Email</label>
950
+ * <input pInputText type="email" formControlName="email" placeholder="example@mail.ru" />
951
+ * @if (form.get('email')?.invalid && form.get('email')?.touched) {
952
+ * <span class="form-error">Введите корректный email</span>
953
+ * }
954
+ * </div>
955
+ * <p-button type="submit" label="Создать пользователя" [loading]="loading" styleClass="btn-full" />
956
+ * <div class="auth-link">
957
+ * <a routerLink="/login">Уже есть аккаунт? Войти</a>
958
+ * </div>
959
+ * </form>
960
+ * </div>
961
+ * </div>
962
+ * ```
963
+ */
964
+ export declare const FE_registerComponent: string;
965
+
966
+ /**
967
+ * **FILE: pages/applications/applications.component.ts** и **.html** — список заявок пользователя
968
+ *
969
+ * МЕНЯТЬ: поля `statusLabels`, `statusSeverities`, `paymentLabels` под свои статусы/поля
970
+ * @example
971
+ * ```typescript
972
+ * // applications.component.ts
973
+ * import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
974
+ * import { Router, RouterLink } from '@angular/router';
975
+ * import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
976
+ * import { SlicePipe } from '@angular/common';
977
+ * import { ButtonModule } from 'primeng/button';
978
+ * import { CardModule } from 'primeng/card';
979
+ * import { TagModule } from 'primeng/tag';
980
+ * import { DialogModule } from 'primeng/dialog';
981
+ * import { TextareaModule } from 'primeng/textarea';
982
+ * import { MessageService } from 'primeng/api';
983
+ * import { ApplicationsService } from '../../services/applications.service';
984
+ * import { ReviewsService } from '../../services/reviews.service';
985
+ * import { AuthService } from '../../services/auth.service';
986
+ *
987
+ * @Component({
988
+ * selector: 'app-applications',
989
+ * standalone: true,
990
+ * imports: [ReactiveFormsModule, ButtonModule, CardModule, TagModule, DialogModule, TextareaModule, RouterLink, SlicePipe],
991
+ * templateUrl: './applications.component.html',
992
+ * })
993
+ * export class ApplicationsComponent implements OnInit {
994
+ * applications: any[] = [];
995
+ * reviewForm: FormGroup;
996
+ * reviewDialogVisible = false;
997
+ * selectedAppId: number | null = null;
998
+ *
999
+ * // МЕНЯТЬ: метки и цвета статусов под свою тему
1000
+ * statusLabels: Record<string, string> = {
1001
+ * new: 'Новая',
1002
+ * in_progress: 'Идет обучение',
1003
+ * completed: 'Обучение завершено',
1004
+ * };
1005
+ * statusSeverities: Record<string, 'info' | 'warn' | 'success' | 'danger' | 'secondary'> = {
1006
+ * new: 'info',
1007
+ * in_progress: 'warn',
1008
+ * completed: 'success',
1009
+ * };
1010
+ * // МЕНЯТЬ: метки способов оплаты / других перечислений
1011
+ * paymentLabels: Record<string, string> = {
1012
+ * cash: 'Наличными',
1013
+ * transfer: 'Перевод по номеру телефона',
1014
+ * };
1015
+ *
1016
+ * constructor(
1017
+ * private appsSvc: ApplicationsService,
1018
+ * private reviewsSvc: ReviewsService,
1019
+ * private auth: AuthService,
1020
+ * private fb: FormBuilder,
1021
+ * private router: Router,
1022
+ * private msg: MessageService,
1023
+ * private cdr: ChangeDetectorRef, // ВАЖНО: нужен для обновления после HTTP
1024
+ * ) {
1025
+ * this.reviewForm = this.fb.group({
1026
+ * review_text: ['', [Validators.required, Validators.minLength(3)]],
1027
+ * });
1028
+ * }
1029
+ *
1030
+ * ngOnInit() { this.load(); }
1031
+ *
1032
+ * load() {
1033
+ * this.appsSvc.getMy().subscribe({
1034
+ * next: (data) => {
1035
+ * this.applications = data;
1036
+ * this.cdr.detectChanges(); // ВАЖНО: без этого данные могут не отобразиться
1037
+ * },
1038
+ * error: (err) => console.error('Ошибка загрузки заявок:', err),
1039
+ * });
1040
+ * }
1041
+ *
1042
+ * openReviewDialog(appId: number) {
1043
+ * this.selectedAppId = appId;
1044
+ * this.reviewForm.reset();
1045
+ * this.reviewDialogVisible = true;
1046
+ * }
1047
+ *
1048
+ * submitReview() {
1049
+ * if (this.reviewForm.invalid || !this.selectedAppId) return;
1050
+ * this.reviewsSvc.create({
1051
+ * application_id: this.selectedAppId,
1052
+ * review_text: this.reviewForm.value.review_text,
1053
+ * }).subscribe({
1054
+ * next: () => {
1055
+ * this.msg.add({ severity: 'success', summary: 'Отзыв отправлен' });
1056
+ * this.reviewDialogVisible = false;
1057
+ * this.load();
1058
+ * },
1059
+ * error: () => this.msg.add({ severity: 'error', summary: 'Ошибка при отправке отзыва' }),
1060
+ * });
1061
+ * }
1062
+ *
1063
+ * logout() { this.auth.logout(); }
1064
+ * }
1065
+ * ```
1066
+ * @example
1067
+ * ```html
1068
+ * <!-- applications.component.html — МЕНЯТЬ: названия полей app.course_name, app.desired_start_date, app.payment_method -->
1069
+ * <div class="navbar">
1070
+ * <span class="navbar-title">Образовательный портал</span>
1071
+ * <div class="flex gap-2">
1072
+ * <p-button label="Новая заявка" icon="pi pi-plus" routerLink="/applications/new" />
1073
+ * <p-button label="Выйти" severity="secondary" icon="pi pi-sign-out" (click)="logout()" />
1074
+ * </div>
1075
+ * </div>
1076
+ * <div class="page-content">
1077
+ * <h1 class="page-title">Мои заявки</h1>
1078
+ * @if (applications.length === 0) {
1079
+ * <div class="app-card"><p style="text-align:center; color:#666;">У вас пока нет заявок.</p></div>
1080
+ * }
1081
+ * @for (app of applications; track app.id) {
1082
+ * <div class="app-card">
1083
+ * <div class="app-card-header">
1084
+ * <span class="app-course-name">{{ app.course_name }}</span>
1085
+ * <span class="app-status app-status--{{ app.status }}">{{ statusLabels[app.status] }}</span>
1086
+ * </div>
1087
+ * <div class="app-card-body">
1088
+ * <p>Дата начала: <strong>{{ app.desired_start_date | slice:0:10 }}</strong></p>
1089
+ * <p>Оплата: <strong>{{ paymentLabels[app.payment_method] }}</strong></p>
1090
+ * <p class="app-created">Создана: {{ app.created_at | slice:0:10 }}</p>
1091
+ * </div>
1092
+ * @if (app.review_text) {
1093
+ * <div class="review-box"><strong>Ваш отзыв:</strong> {{ app.review_text }}</div>
1094
+ * } @else {
1095
+ * <p-button label="Оставить отзыв" severity="secondary" size="small" icon="pi pi-comment" (click)="openReviewDialog(app.id)" />
1096
+ * }
1097
+ * </div>
1098
+ * }
1099
+ * </div>
1100
+ * <p-dialog header="Оставить отзыв" [(visible)]="reviewDialogVisible" [modal]="true" [style]="{width: '450px'}">
1101
+ * <form [formGroup]="reviewForm" (ngSubmit)="submitReview()" class="flex flex-col gap-3">
1102
+ * <textarea pTextarea formControlName="review_text" rows="4" placeholder="Напишите ваш отзыв..." class="w-full"></textarea>
1103
+ * <div class="flex justify-end gap-2">
1104
+ * <p-button label="Отмена" severity="secondary" type="button" (click)="reviewDialogVisible = false" />
1105
+ * <p-button label="Отправить" type="submit" />
1106
+ * </div>
1107
+ * </form>
1108
+ * </p-dialog>
1109
+ * ```
1110
+ */
1111
+ export declare const FE_applicationsComponent: string;
1112
+
1113
+ /**
1114
+ * **FILE: pages/create-application/create-application.component.ts** и **.html** — форма создания заявки
1115
+ *
1116
+ * МЕНЯТЬ: поля формы `course_name`, `desired_start_date`, `payment_method` и radiobutton значения
1117
+ * @example
1118
+ * ```typescript
1119
+ * // create-application.component.ts
1120
+ * import { Component } from '@angular/core';
1121
+ * import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
1122
+ * import { Router, RouterLink } from '@angular/router';
1123
+ * import { InputTextModule } from 'primeng/inputtext';
1124
+ * import { ButtonModule } from 'primeng/button';
1125
+ * import { CardModule } from 'primeng/card';
1126
+ * import { RadioButtonModule } from 'primeng/radiobutton';
1127
+ * import { DatePickerModule } from 'primeng/datepicker';
1128
+ * import { MessageService } from 'primeng/api';
1129
+ * import { ApplicationsService } from '../../services/applications.service';
1130
+ *
1131
+ * @Component({
1132
+ * selector: 'app-create-application',
1133
+ * standalone: true,
1134
+ * imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, RadioButtonModule, DatePickerModule, RouterLink],
1135
+ * templateUrl: './create-application.component.html',
1136
+ * })
1137
+ * export class CreateApplicationComponent {
1138
+ * form: FormGroup;
1139
+ * loading = false;
1140
+ *
1141
+ * constructor(private fb: FormBuilder, private appsSvc: ApplicationsService, private router: Router, private msg: MessageService) {
1142
+ * this.form = this.fb.group({
1143
+ * course_name: ['', Validators.required], // МЕНЯТЬ: поле 1
1144
+ * desired_start_date: [null, Validators.required], // МЕНЯТЬ: поле даты
1145
+ * payment_method: ['cash', Validators.required], // МЕНЯТЬ: поле с вариантами
1146
+ * });
1147
+ * }
1148
+ *
1149
+ * submit() {
1150
+ * if (this.form.invalid) { this.form.markAllAsTouched(); return; }
1151
+ * this.loading = true;
1152
+ * const val = this.form.value;
1153
+ * // ВАЖНО: конвертация Date -> строка YYYY-MM-DD для API
1154
+ * const date: Date = val.desired_start_date;
1155
+ * const dateStr = `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
1156
+ * this.appsSvc.create({ ...val, desired_start_date: dateStr }).subscribe({
1157
+ * next: () => {
1158
+ * this.msg.add({ severity: 'success', summary: 'Заявка отправлена на рассмотрение' });
1159
+ * setTimeout(() => this.router.navigate(['/applications']), 1000);
1160
+ * },
1161
+ * error: () => {
1162
+ * this.msg.add({ severity: 'error', summary: 'Ошибка при создании заявки' });
1163
+ * this.loading = false;
1164
+ * },
1165
+ * });
1166
+ * }
1167
+ * }
1168
+ * ```
1169
+ * @example
1170
+ * ```html
1171
+ * <!-- create-application.component.html — МЕНЯТЬ: поля формы, label, placeholder, radiobutton значения -->
1172
+ * <div class="auth-wrapper">
1173
+ * <div class="auth-card" style="max-width:480px">
1174
+ * <h2>Новая заявка на обучение</h2>
1175
+ * <form [formGroup]="form" (ngSubmit)="submit()">
1176
+ * <div class="form-group">
1177
+ * <label>Наименование курса</label>
1178
+ * <input pInputText formControlName="course_name" placeholder="Введите название курса" />
1179
+ * @if (form.get('course_name')?.invalid && form.get('course_name')?.touched) {
1180
+ * <span class="form-error">Введите название курса</span>
1181
+ * }
1182
+ * </div>
1183
+ * <div class="form-group">
1184
+ * <label>Желаемая дата начала обучения</label>
1185
+ * <p-datepicker formControlName="desired_start_date" dateFormat="dd.mm.yy" [showIcon]="true" placeholder="Выберите дату" styleClass="w-full" />
1186
+ * @if (form.get('desired_start_date')?.invalid && form.get('desired_start_date')?.touched) {
1187
+ * <span class="form-error">Укажите дату</span>
1188
+ * }
1189
+ * </div>
1190
+ * <div class="form-group">
1191
+ * <label>Способ оплаты</label>
1192
+ * <div class="radio-group">
1193
+ * <div class="radio-option">
1194
+ * <p-radiobutton formControlName="payment_method" value="cash" inputId="cash" />
1195
+ * <label for="cash">Наличными</label>
1196
+ * </div>
1197
+ * <div class="radio-option">
1198
+ * <p-radiobutton formControlName="payment_method" value="transfer" inputId="transfer" />
1199
+ * <label for="transfer">Переводом по номеру телефона</label>
1200
+ * </div>
1201
+ * </div>
1202
+ * </div>
1203
+ * <div class="btn-row">
1204
+ * <p-button label="Отмена" severity="secondary" type="button" routerLink="/applications" styleClass="btn-half" />
1205
+ * <p-button label="Отправить" type="submit" [loading]="loading" styleClass="btn-half" />
1206
+ * </div>
1207
+ * </form>
1208
+ * </div>
1209
+ * </div>
1210
+ * ```
1211
+ */
1212
+ export declare const FE_createAppComponent: string;
1213
+
1214
+ /**
1215
+ * **FILE: pages/admin/admin.component.ts** и **.html** — панель администратора
1216
+ *
1217
+ * МЕНЯТЬ: `statusOptions`, `statusLabels`, `paymentLabels` под свою тему
1218
+ * @example
1219
+ * ```typescript
1220
+ * // admin.component.ts
1221
+ * import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
1222
+ * import { SlicePipe } from '@angular/common';
1223
+ * import { ButtonModule } from 'primeng/button';
1224
+ * import { SelectModule } from 'primeng/select';
1225
+ * import { FormsModule } from '@angular/forms';
1226
+ * import { MessageService } from 'primeng/api';
1227
+ * import { ApplicationsService } from '../../services/applications.service';
1228
+ * import { AuthService } from '../../services/auth.service';
1229
+ *
1230
+ * @Component({
1231
+ * selector: 'app-admin',
1232
+ * standalone: true,
1233
+ * imports: [ButtonModule, SelectModule, FormsModule, SlicePipe],
1234
+ * templateUrl: './admin.component.html',
1235
+ * })
1236
+ * export class AdminComponent implements OnInit {
1237
+ * applications: any[] = [];
1238
+ *
1239
+ * // МЕНЯТЬ: варианты статусов
1240
+ * statusOptions = [
1241
+ * { label: 'Новая', value: 'new' },
1242
+ * { label: 'Идет обучение', value: 'in_progress' },
1243
+ * { label: 'Обучение завершено', value: 'completed' },
1244
+ * ];
1245
+ * statusLabels: Record<string, string> = { new: 'Новая', in_progress: 'Идет обучение', completed: 'Обучение завершено' };
1246
+ * statusSeverities: Record<string, 'info' | 'warn' | 'success' | 'danger' | 'secondary'> = {
1247
+ * new: 'info', in_progress: 'warn', completed: 'success',
1248
+ * };
1249
+ * paymentLabels: Record<string, string> = { cash: 'Наличными', transfer: 'Перевод по номеру телефона' };
1250
+ *
1251
+ * constructor(private appsSvc: ApplicationsService, private auth: AuthService, private msg: MessageService, private cdr: ChangeDetectorRef) {}
1252
+ *
1253
+ * ngOnInit() { this.load(); }
1254
+ *
1255
+ * load() {
1256
+ * this.appsSvc.getAll().subscribe({
1257
+ * next: (data) => { this.applications = data; this.cdr.detectChanges(); },
1258
+ * error: (err) => console.error('Ошибка загрузки:', err),
1259
+ * });
1260
+ * }
1261
+ *
1262
+ * changeStatus(app: any, newStatus: string) {
1263
+ * this.appsSvc.updateStatus(app.id, newStatus).subscribe({
1264
+ * next: (updated: any) => {
1265
+ * app.status = updated.status;
1266
+ * this.msg.add({ severity: 'success', summary: 'Статус обновлён' });
1267
+ * this.cdr.detectChanges();
1268
+ * },
1269
+ * error: () => this.msg.add({ severity: 'error', summary: 'Ошибка обновления' }),
1270
+ * });
1271
+ * }
1272
+ *
1273
+ * logout() { this.auth.logout(); }
1274
+ * }
1275
+ * ```
1276
+ * @example
1277
+ * ```html
1278
+ * <!-- admin.component.html — МЕНЯТЬ: поля app.course_name, app.desired_start_date, app.payment_method, app.full_name -->
1279
+ * <div class="navbar">
1280
+ * <span class="navbar-title">Образовательный портал — Администратор</span>
1281
+ * <p-button label="Выйти" severity="secondary" icon="pi pi-sign-out" (click)="logout()" />
1282
+ * </div>
1283
+ * <div class="page-content">
1284
+ * <h1 class="page-title">Все заявки</h1>
1285
+ * @if (applications.length === 0) {
1286
+ * <div class="app-card"><p style="text-align:center; color:#666;">Заявок пока нет</p></div>
1287
+ * }
1288
+ * @for (app of applications; track app.id) {
1289
+ * <div class="app-card">
1290
+ * <div class="app-card-header">
1291
+ * <span class="app-course-name">{{ app.course_name }}</span>
1292
+ * <span class="app-status app-status--{{ app.status }}">{{ statusLabels[app.status] }}</span>
1293
+ * </div>
1294
+ * <div class="app-card-body">
1295
+ * <p>Пользователь: <strong>{{ app.full_name }}</strong> ({{ app.login }})</p>
1296
+ * <p>Email: <strong>{{ app.email }}</strong></p>
1297
+ * <p>Телефон: <strong>{{ app.phone }}</strong></p>
1298
+ * <p>Дата начала: <strong>{{ app.desired_start_date | slice:0:10 }}</strong></p>
1299
+ * <p>Оплата: <strong>{{ paymentLabels[app.payment_method] }}</strong></p>
1300
+ * <p class="app-created">Создана: {{ app.created_at | slice:0:10 }}</p>
1301
+ * </div>
1302
+ * <div class="admin-status-row">
1303
+ * <label>Изменить статус:</label>
1304
+ * <p-select [options]="statusOptions" [(ngModel)]="app.status" optionLabel="label" optionValue="value" (onChange)="changeStatus(app, app.status)" />
1305
+ * </div>
1306
+ * </div>
1307
+ * }
1308
+ * </div>
1309
+ * ```
1310
+ */
1311
+ export declare const FE_adminComponent: string;
1312
+
1313
+ /**
1314
+ * **FILE: lib/src/styles.scss** — глобальные стили (PrimeNG + кастомные)
1315
+ *
1316
+ * НЕ МЕНЯТЬ (можно менять цвет --primary для другой темы)
1317
+ * @example
1318
+ * ```scss
1319
+ * @import "primeicons/primeicons.css";
1320
+ *
1321
+ * :root {
1322
+ * --primary: #2563eb; // МЕНЯТЬ: основной цвет
1323
+ * --primary-hover: #1d4ed8;
1324
+ * --primary-light: #eff6ff;
1325
+ * --bg: #f1f5f9;
1326
+ * --card: #ffffff;
1327
+ * --border: #e2e8f0;
1328
+ * --text: #1e293b;
1329
+ * --text-muted: #64748b;
1330
+ * }
1331
+ *
1332
+ * * { box-sizing: border-box; margin: 0; padding: 0; }
1333
+ * body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 15px; }
1334
+ *
1335
+ * .p-button { background: var(--primary) !important; border-color: var(--primary) !important; color: #fff !important; border-radius: 6px !important; }
1336
+ * .p-button:hover { background: var(--primary-hover) !important; }
1337
+ * .p-button.p-button-secondary { background: #fff !important; border-color: var(--border) !important; color: var(--text) !important; }
1338
+ *
1339
+ * .p-inputtext { border: 1px solid var(--border) !important; border-radius: 6px !important; padding: 0.5rem 0.75rem !important; color: var(--text) !important; background: #fff !important; width: 100%; }
1340
+ * .p-inputtext:focus { border-color: var(--primary) !important; box-shadow: 0 0 0 3px rgba(37,99,235,0.12) !important; outline: none !important; }
1341
+ *
1342
+ * .navbar { display: flex; align-items: center; justify-content: space-between; padding: 0 2rem; height: 60px; background: #fff; border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; }
1343
+ * .navbar-title { font-size: 1rem; font-weight: 600; color: var(--primary); }
1344
+ *
1345
+ * .page-content { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem; }
1346
+ * .page-title { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; }
1347
+ *
1348
+ * .app-card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
1349
+ * .app-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; margin-bottom: 0.75rem; }
1350
+ * .app-course-name { font-size: 1.05rem; font-weight: 600; color: var(--text); }
1351
+ * .app-card-body { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 1rem; color: var(--text-muted); font-size: 0.875rem; }
1352
+ *
1353
+ * .app-status { display: inline-block; padding: 0.25rem 0.65rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
1354
+ * .app-status--new { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; }
1355
+ * .app-status--in_progress { background: #fffbeb; color: #d97706; border: 1px solid #fde68a; }
1356
+ * .app-status--completed { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; }
1357
+ *
1358
+ * .auth-wrapper { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: var(--bg); padding: 2rem; }
1359
+ * .auth-card { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 2rem; box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
1360
+ * .form-group { display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 1rem; }
1361
+ * .form-error { color: #dc2626; font-size: 0.8rem; }
1362
+ *
1363
+ * .radio-group { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.25rem; }
1364
+ * .radio-option { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
1365
+ * .p-radiobutton .p-radiobutton-box { border: 2px solid #cbd5e1 !important; background: #fff !important; width: 18px !important; height: 18px !important; border-radius: 50% !important; }
1366
+ * .p-radiobutton.p-radiobutton-checked .p-radiobutton-box { border-color: var(--primary) !important; }
1367
+ * .p-radiobutton.p-radiobutton-checked .p-radiobutton-box .p-radiobutton-icon { background: var(--primary) !important; width: 8px !important; height: 8px !important; border-radius: 50% !important; }
1368
+ *
1369
+ * .p-select { border: 1px solid var(--border) !important; border-radius: 6px !important; background: #fff !important; }
1370
+ * .p-select-overlay { background: #fff !important; border: 1px solid var(--border) !important; border-radius: 8px !important; }
1371
+ * .p-select-option { background: #fff !important; color: var(--text) !important; }
1372
+ * .p-select-option.p-select-option-selected { background: var(--primary) !important; color: #fff !important; }
1373
+ *
1374
+ * .p-dialog { border: 1px solid var(--border) !important; border-radius: 12px !important; background: #fff !important; }
1375
+ * .p-dialog .p-dialog-header { border-bottom: 1px solid var(--border) !important; font-weight: 600 !important; background: #fff !important; }
1376
+ * .p-dialog .p-dialog-content { background: #fff !important; color: var(--text) !important; padding: 1.25rem !important; }
1377
+ *
1378
+ * .btn-full { width: 100%; margin-top: 0.5rem; }
1379
+ * .btn-full .p-button { width: 100%; justify-content: center; }
1380
+ * .btn-row { display: flex; gap: 0.75rem; margin-top: 0.5rem; }
1381
+ * .btn-half { flex: 1; }
1382
+ * .btn-half .p-button { width: 100%; justify-content: center; }
1383
+ *
1384
+ * .admin-status-row { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
1385
+ *
1386
+ * .review-box { padding: 0.75rem 1rem; background: #f0fdf4; border-left: 3px solid #16a34a; border-radius: 0 6px 6px 0; font-size: 0.875rem; color: #166534; }
1387
+ * ```
1388
+ */
1389
+ export declare const FE_styles: string;