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.
- package/index.d.ts +1389 -0
- package/index.js +79 -0
- package/lib/src/app/app.config.ts +20 -0
- package/lib/src/app/app.routes.ts +30 -0
- package/lib/src/app/app.ts +15 -0
- package/lib/src/app/guards/admin.guard.ts +13 -0
- package/lib/src/app/guards/auth.guard.ts +13 -0
- package/lib/src/app/interceptors/auth.interceptor.ts +12 -0
- package/lib/src/app/pages/admin/admin.component.html +52 -0
- package/lib/src/app/pages/admin/admin.component.ts +86 -0
- package/lib/src/app/pages/applications/applications.component.html +72 -0
- package/lib/src/app/pages/applications/applications.component.ts +105 -0
- package/lib/src/app/pages/create-application/create-application.component.html +61 -0
- package/lib/src/app/pages/create-application/create-application.component.ts +67 -0
- package/lib/src/app/pages/login/login.component.html +38 -0
- package/lib/src/app/pages/login/login.component.ts +47 -0
- package/lib/src/app/pages/register/register.component.html +63 -0
- package/lib/src/app/pages/register/register.component.ts +56 -0
- package/lib/src/app/services/applications.service.ts +38 -0
- package/lib/src/app/services/auth.service.ts +61 -0
- package/lib/src/app/services/reviews.service.ts +23 -0
- package/lib/src/styles.scss +358 -0
- package/native/src/app.module.ts +16 -0
- package/native/src/applications/applications.controller.ts +47 -0
- package/native/src/applications/applications.module.ts +12 -0
- package/native/src/applications/applications.service.ts +71 -0
- package/native/src/auth/auth.controller.ts +26 -0
- package/native/src/auth/auth.module.ts +19 -0
- package/native/src/auth/auth.service.ts +62 -0
- package/native/src/auth/jwt-auth.guard.ts +8 -0
- package/native/src/auth/jwt.strategy.ts +20 -0
- package/native/src/database/database.module.ts +26 -0
- package/native/src/database/init.sql +45 -0
- package/native/src/main.ts +11 -0
- package/native/src/reviews/reviews.controller.ts +30 -0
- package/native/src/reviews/reviews.module.ts +12 -0
- package/native/src/reviews/reviews.service.ts +35 -0
- 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;
|