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
@@ -0,0 +1,38 @@
1
+ <!-- Страница входа. МЕНЯТЬ: заголовки и тексты под свою тему -->
2
+ <div class="auth-wrapper">
3
+ <div class="auth-card">
4
+ <!-- МЕНЯТЬ: заголовок страницы -->
5
+ <h2>Вход в систему</h2>
6
+
7
+ @if (error) {
8
+ <div class="form-error" style="margin-bottom:1rem; padding:0.5rem; border:1px solid #cc0000; border-radius:4px;">
9
+ {{ error }}
10
+ </div>
11
+ }
12
+
13
+ <form [formGroup]="form" (ngSubmit)="submit()">
14
+ <!-- Поля login и password — не менять, они всегда нужны -->
15
+ <div class="form-group">
16
+ <label>Логин</label>
17
+ <input pInputText formControlName="login" placeholder="Введите логин" />
18
+ @if (form.get('login')?.invalid && form.get('login')?.touched) {
19
+ <span class="form-error">Введите логин</span>
20
+ }
21
+ </div>
22
+
23
+ <div class="form-group">
24
+ <label>Пароль</label>
25
+ <input pInputText type="password" formControlName="password" placeholder="Введите пароль" />
26
+ @if (form.get('password')?.invalid && form.get('password')?.touched) {
27
+ <span class="form-error">Введите пароль</span>
28
+ }
29
+ </div>
30
+
31
+ <p-button type="submit" label="Войти" [loading]="loading" styleClass="btn-full" />
32
+
33
+ <div class="auth-link">
34
+ <a routerLink="/register">Еще не зарегистрированы? Регистрация</a>
35
+ </div>
36
+ </form>
37
+ </div>
38
+ </div>
@@ -0,0 +1,47 @@
1
+ import { Component } from '@angular/core';
2
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { InputTextModule } from 'primeng/inputtext';
5
+ import { ButtonModule } from 'primeng/button';
6
+ import { CardModule } from 'primeng/card';
7
+ import { MessageModule } from 'primeng/message';
8
+ import { AuthService } from '../../services/auth.service';
9
+
10
+ // Страница входа — не менять структуру
11
+ // МЕНЯТЬ: маршруты навигации после успешного входа (сейчас /admin и /applications)
12
+ @Component({
13
+ selector: 'app-login',
14
+ standalone: true,
15
+ imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, MessageModule, RouterLink],
16
+ templateUrl: './login.component.html',
17
+ })
18
+ export class LoginComponent {
19
+ form: FormGroup;
20
+ error = '';
21
+ loading = false;
22
+
23
+ constructor(private fb: FormBuilder, private auth: AuthService, private router: Router) {
24
+ this.form = this.fb.group({
25
+ login: ['', Validators.required],
26
+ password: ['', Validators.required],
27
+ });
28
+ }
29
+
30
+ submit() {
31
+ if (this.form.invalid) return;
32
+ this.loading = true;
33
+ this.error = '';
34
+ const { login, password } = this.form.value;
35
+ this.auth.login(login, password).subscribe({
36
+ next: (res) => {
37
+ // МЕНЯТЬ: маршруты редиректа после входа под своё приложение
38
+ if (res.user.is_admin) this.router.navigate(['/admin']);
39
+ else this.router.navigate(['/applications']);
40
+ },
41
+ error: (err) => {
42
+ this.error = err.error?.message || 'Неверный логин или пароль';
43
+ this.loading = false;
44
+ },
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,63 @@
1
+ <!-- Форма регистрации. МЕНЯТЬ: заголовки, подписи полей, placeholder-ы под свою тему -->
2
+ <!-- Поля: login, password, full_name, phone, email — соответствуют таблице users в БД -->
3
+ <div class="auth-wrapper">
4
+ <div class="auth-card" style="max-width:480px">
5
+ <!-- МЕНЯТЬ: заголовок -->
6
+ <h2>Регистрация</h2>
7
+
8
+ @if (error) {
9
+ <div class="form-error" style="margin-bottom:1rem; padding:0.5rem; border:1px solid #cc0000; border-radius:4px;">
10
+ {{ error }}
11
+ </div>
12
+ }
13
+
14
+ <form [formGroup]="form" (ngSubmit)="submit()">
15
+
16
+ <div class="form-group">
17
+ <label>Логин <small>(латиница и цифры, от 6 символов)</small></label>
18
+ <input pInputText formControlName="login" placeholder="mylogin123" />
19
+ @if (form.get('login')?.invalid && form.get('login')?.touched) {
20
+ <span class="form-error">Только латиница и цифры, минимум 6 символов</span>
21
+ }
22
+ </div>
23
+
24
+ <div class="form-group">
25
+ <label>Пароль <small>(минимум 8 символов)</small></label>
26
+ <input pInputText type="password" formControlName="password" placeholder="Минимум 8 символов" />
27
+ @if (form.get('password')?.invalid && form.get('password')?.touched) {
28
+ <span class="form-error">Минимум 8 символов</span>
29
+ }
30
+ </div>
31
+
32
+ <div class="form-group">
33
+ <label>ФИО <small>(кириллица и пробелы)</small></label>
34
+ <input pInputText formControlName="full_name" placeholder="Иванов Иван Иванович" />
35
+ @if (form.get('full_name')?.invalid && form.get('full_name')?.touched) {
36
+ <span class="form-error">Только кириллица и пробелы</span>
37
+ }
38
+ </div>
39
+
40
+ <div class="form-group">
41
+ <label>Телефон <small>(формат: 8(XXX)XXX-XX-XX)</small></label>
42
+ <input pInputText formControlName="phone" placeholder="8(999)123-45-67" />
43
+ @if (form.get('phone')?.invalid && form.get('phone')?.touched) {
44
+ <span class="form-error">Формат: 8(XXX)XXX-XX-XX</span>
45
+ }
46
+ </div>
47
+
48
+ <div class="form-group">
49
+ <label>Email</label>
50
+ <input pInputText type="email" formControlName="email" placeholder="example@mail.ru" />
51
+ @if (form.get('email')?.invalid && form.get('email')?.touched) {
52
+ <span class="form-error">Введите корректный email</span>
53
+ }
54
+ </div>
55
+
56
+ <p-button type="submit" label="Создать пользователя" [loading]="loading" styleClass="btn-full" />
57
+
58
+ <div class="auth-link">
59
+ <a routerLink="/login">Уже есть аккаунт? Войти</a>
60
+ </div>
61
+ </form>
62
+ </div>
63
+ </div>
@@ -0,0 +1,56 @@
1
+ import { Component } from '@angular/core';
2
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
3
+ import { Router, RouterLink } from '@angular/router';
4
+ import { InputTextModule } from 'primeng/inputtext';
5
+ import { ButtonModule } from 'primeng/button';
6
+ import { CardModule } from 'primeng/card';
7
+ import { MessageModule } from 'primeng/message';
8
+ import { AuthService } from '../../services/auth.service';
9
+
10
+ // Страница регистрации
11
+ // Поля формы берутся из таблицы users в БД: login, password, full_name, phone, email
12
+ // Не менять структуру — только тексты/подсказки при желании
13
+ @Component({
14
+ selector: 'app-register',
15
+ standalone: true,
16
+ imports: [ReactiveFormsModule, InputTextModule, ButtonModule, CardModule, MessageModule, RouterLink],
17
+ templateUrl: './register.component.html',
18
+ })
19
+ export class RegisterComponent {
20
+ form: FormGroup;
21
+ error = '';
22
+ loading = false;
23
+
24
+ constructor(private fb: FormBuilder, private auth: AuthService, private router: Router) {
25
+ // Валидация полей:
26
+ // login — только латиница и цифры, минимум 6 символов
27
+ // password — минимум 8 символов
28
+ // full_name — только кириллица и пробелы
29
+ // phone — формат 8(XXX)XXX-XX-XX
30
+ // email — стандартный email
31
+ this.form = this.fb.group({
32
+ login: ['', [Validators.required, Validators.minLength(6), Validators.pattern(/^[a-zA-Z0-9]+$/)]],
33
+ password: ['', [Validators.required, Validators.minLength(8)]],
34
+ full_name: ['', [Validators.required, Validators.pattern(/^[А-ЯЁа-яё\s]+$/)]],
35
+ phone: ['', [Validators.required, Validators.pattern(/^8\(\d{3}\)\d{3}-\d{2}-\d{2}$/)]],
36
+ email: ['', [Validators.required, Validators.email]],
37
+ });
38
+ }
39
+
40
+ submit() {
41
+ if (this.form.invalid) {
42
+ this.form.markAllAsTouched(); // показать все ошибки
43
+ return;
44
+ }
45
+ this.loading = true;
46
+ this.error = '';
47
+ this.auth.register(this.form.value).subscribe({
48
+ // МЕНЯТЬ: маршрут после регистрации (сейчас /applications)
49
+ next: () => this.router.navigate(['/applications']),
50
+ error: (err) => {
51
+ this.error = err.error?.message || 'Ошибка регистрации';
52
+ this.loading = false;
53
+ },
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,38 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+
4
+ // МЕНЯТЬ: порт если бэкенд на другом порту
5
+ const API = 'http://localhost:3000';
6
+
7
+ // МЕНЯТЬ: переименовать класс (например OrdersService, ProductsService)
8
+ @Injectable({ providedIn: 'root' })
9
+ export class ApplicationsService {
10
+ constructor(private http: HttpClient) {}
11
+
12
+ // POST /applications — создать новую заявку
13
+ // Не менять сигнатуру, data формируется в компоненте
14
+ create(data: any) {
15
+ return this.http.post(`${API}/applications`, data);
16
+ }
17
+
18
+ // GET /applications/my — получить МОИ заявки (авторизованный пользователь)
19
+ getMy() {
20
+ return this.http.get<any[]>(`${API}/applications/my`);
21
+ }
22
+
23
+ // GET /applications — получить ВСЕ заявки (только для администратора)
24
+ getAll() {
25
+ return this.http.get<any[]>(`${API}/applications`);
26
+ }
27
+
28
+ // PATCH /applications/:id/status — сменить статус заявки
29
+ // МЕНЯТЬ: 'status' если обновляется другое поле
30
+ updateStatus(id: number, status: string) {
31
+ return this.http.patch(`${API}/applications/${id}/status`, { status });
32
+ }
33
+
34
+ // DELETE /applications/:id — удалить заявку
35
+ delete(id: number) {
36
+ return this.http.delete(`${API}/applications/${id}`);
37
+ }
38
+ }
@@ -0,0 +1,61 @@
1
+ import { Injectable, signal } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Router } from '@angular/router';
4
+ import { tap } from 'rxjs';
5
+
6
+ // МЕНЯТЬ: порт если бэкенд на другом порту
7
+ const API = 'http://localhost:3000';
8
+
9
+ @Injectable({ providedIn: 'root' })
10
+ export class AuthService {
11
+ // signal() — реактивное состояние. Доступен в любом компоненте через inject(AuthService).currentUser()
12
+ // Не менять
13
+ currentUser = signal<any>(null);
14
+
15
+ constructor(private http: HttpClient, private router: Router) {
16
+ // При перезагрузке страницы восстанавливаем пользователя из localStorage
17
+ const stored = localStorage.getItem('user');
18
+ if (stored) this.currentUser.set(JSON.parse(stored));
19
+ }
20
+
21
+ // POST /auth/register — регистрация нового пользователя
22
+ // Не менять — поля приходят из формы регистрации
23
+ register(data: any) {
24
+ return this.http.post<any>(`${API}/auth/register`, data).pipe(
25
+ tap(res => this.saveSession(res)),
26
+ );
27
+ }
28
+
29
+ // POST /auth/login — вход по логину и паролю
30
+ // Не менять
31
+ login(login: string, password: string) {
32
+ return this.http.post<any>(`${API}/auth/login`, { login, password }).pipe(
33
+ tap(res => this.saveSession(res)),
34
+ );
35
+ }
36
+
37
+ // Выход — очищаем хранилище и идём на страницу логина
38
+ logout() {
39
+ localStorage.clear();
40
+ this.currentUser.set(null);
41
+ this.router.navigate(['/login']);
42
+ }
43
+
44
+ // Проверяем, является ли текущий пользователь администратором
45
+ isAdmin() {
46
+ return this.currentUser()?.is_admin === true;
47
+ }
48
+
49
+ // Проверяем, авторизован ли пользователь (есть ли JWT в localStorage)
50
+ isLoggedIn() {
51
+ return !!localStorage.getItem('token');
52
+ }
53
+
54
+ // Сохраняем токен и данные пользователя после успешного входа/регистрации
55
+ // Не менять
56
+ private saveSession(res: any) {
57
+ localStorage.setItem('token', res.token);
58
+ localStorage.setItem('user', JSON.stringify(res.user));
59
+ this.currentUser.set(res.user);
60
+ }
61
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+
4
+ // МЕНЯТЬ: порт если бэкенд на другом порту
5
+ const API = 'http://localhost:3000';
6
+
7
+ // МЕНЯТЬ: переименовать (например CommentsService, FeedbackService)
8
+ // МЕНЯТЬ: 'reviews' на название своей сущности во всех URL-ах ниже
9
+ @Injectable({ providedIn: 'root' })
10
+ export class ReviewsService {
11
+ constructor(private http: HttpClient) {}
12
+
13
+ // POST /reviews — оставить отзыв
14
+ // МЕНЯТЬ: поля объекта data под свою тему
15
+ create(data: { application_id: number; review_text: string }) {
16
+ return this.http.post(`${API}/reviews`, data);
17
+ }
18
+
19
+ // GET /reviews/:appId — получить отзывы по конкретной заявке
20
+ getByApplication(appId: number) {
21
+ return this.http.get<any[]>(`${API}/reviews/${appId}`);
22
+ }
23
+ }
@@ -0,0 +1,358 @@
1
+ /* ========================================================
2
+ ГЛОБАЛЬНЫЕ СТИЛИ — styles.scss
3
+ МЕНЯТЬ: цвета в :root под свою тему (просто замените hex-коды)
4
+ Всё остальное менять не нужно — компоненты используют CSS-классы отсюда
5
+ ======================================================== */
6
+
7
+ @import "primeicons/primeicons.css";
8
+
9
+ /* ===== ПЕРЕМЕННЫЕ ===== */
10
+ /* МЕНЯТЬ: --primary — основной цвет кнопок, ссылок, акцентов */
11
+ :root {
12
+ --primary: #2563eb;
13
+ --primary-hover: #1d4ed8;
14
+ --primary-light: #eff6ff;
15
+ --bg: #f1f5f9;
16
+ --card: #ffffff;
17
+ --border: #e2e8f0;
18
+ --text: #1e293b;
19
+ --text-muted: #64748b;
20
+ --danger: #dc2626;
21
+ --success: #16a34a;
22
+ --warning: #d97706;
23
+ }
24
+
25
+ /* ===== СБРОС ===== */
26
+ * { box-sizing: border-box; margin: 0; padding: 0; }
27
+
28
+ body {
29
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ font-size: 15px;
33
+ }
34
+
35
+ a { text-decoration: none; color: var(--primary); }
36
+ a:hover { text-decoration: underline; }
37
+
38
+ /* ===== КНОПКИ ===== */
39
+ .p-button {
40
+ background: var(--primary) !important;
41
+ border-color: var(--primary) !important;
42
+ color: #fff !important;
43
+ border-radius: 6px !important;
44
+ font-weight: 500 !important;
45
+ font-size: 0.9rem !important;
46
+ transition: background 0.15s !important;
47
+ }
48
+ .p-button:hover {
49
+ background: var(--primary-hover) !important;
50
+ border-color: var(--primary-hover) !important;
51
+ }
52
+ .p-button.p-button-secondary {
53
+ background: #fff !important;
54
+ border-color: var(--border) !important;
55
+ color: var(--text) !important;
56
+ }
57
+ .p-button.p-button-secondary:hover {
58
+ background: #f8fafc !important;
59
+ border-color: #cbd5e1 !important;
60
+ }
61
+
62
+ /* ===== ИНПУТЫ ===== */
63
+ .p-inputtext {
64
+ border: 1px solid var(--border) !important;
65
+ border-radius: 6px !important;
66
+ padding: 0.5rem 0.75rem !important;
67
+ color: var(--text) !important;
68
+ background: #fff !important;
69
+ font-size: 0.9rem !important;
70
+ width: 100%;
71
+ transition: border-color 0.15s, box-shadow 0.15s !important;
72
+ }
73
+ .p-inputtext:focus {
74
+ border-color: var(--primary) !important;
75
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12) !important;
76
+ outline: none !important;
77
+ }
78
+
79
+ /* ===== NAVBAR ===== */
80
+ .navbar {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ padding: 0 2rem;
85
+ height: 60px;
86
+ background: #fff;
87
+ border-bottom: 1px solid var(--border);
88
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
89
+ position: sticky;
90
+ top: 0;
91
+ z-index: 100;
92
+ }
93
+ .navbar-title {
94
+ font-size: 1rem;
95
+ font-weight: 600;
96
+ color: var(--primary);
97
+ letter-spacing: -0.01em;
98
+ }
99
+ .navbar .p-button {
100
+ background: var(--primary) !important;
101
+ border-color: var(--primary) !important;
102
+ color: #fff !important;
103
+ }
104
+ .navbar .p-button.p-button-secondary {
105
+ background: #fff !important;
106
+ border-color: var(--border) !important;
107
+ color: var(--text) !important;
108
+ }
109
+
110
+ /* ===== СТРАНИЦА ===== */
111
+ .page-content {
112
+ max-width: 860px;
113
+ margin: 0 auto;
114
+ padding: 2rem 1.5rem;
115
+ }
116
+ .page-title {
117
+ font-size: 1.4rem;
118
+ font-weight: 700;
119
+ color: var(--text);
120
+ margin-bottom: 1.5rem;
121
+ }
122
+
123
+ /* ===== КАРТОЧКИ ЗАЯВОК ===== */
124
+ .app-card {
125
+ background: var(--card);
126
+ border: 1px solid var(--border);
127
+ border-radius: 10px;
128
+ padding: 1.25rem 1.5rem;
129
+ margin-bottom: 1rem;
130
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
131
+ transition: box-shadow 0.15s;
132
+ }
133
+ .app-card:hover {
134
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
135
+ }
136
+ .app-card-header {
137
+ display: flex;
138
+ justify-content: space-between;
139
+ align-items: flex-start;
140
+ gap: 1rem;
141
+ margin-bottom: 0.75rem;
142
+ }
143
+ .app-course-name {
144
+ font-size: 1.05rem;
145
+ font-weight: 600;
146
+ color: var(--text);
147
+ }
148
+ .app-card-body {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 0.3rem;
152
+ margin-bottom: 1rem;
153
+ color: var(--text-muted);
154
+ font-size: 0.875rem;
155
+ }
156
+ .app-card-body strong { color: var(--text); }
157
+ .app-created { font-size: 0.8rem; color: #94a3b8; margin-top: 0.2rem; }
158
+
159
+ /* Статус-бейджи */
160
+ .app-status {
161
+ display: inline-block;
162
+ padding: 0.25rem 0.65rem;
163
+ border-radius: 20px;
164
+ font-size: 0.75rem;
165
+ font-weight: 600;
166
+ white-space: nowrap;
167
+ }
168
+ .app-status--new { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; }
169
+ .app-status--in_progress { background: #fffbeb; color: #d97706; border: 1px solid #fde68a; }
170
+ .app-status--completed { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; }
171
+
172
+ /* ===== ОТЗЫВ ===== */
173
+ .review-box {
174
+ padding: 0.75rem 1rem;
175
+ background: #f0fdf4;
176
+ border-left: 3px solid #16a34a;
177
+ border-radius: 0 6px 6px 0;
178
+ font-size: 0.875rem;
179
+ color: #166534;
180
+ }
181
+
182
+ /* ===== ФОРМЫ (авторизация / регистрация) ===== */
183
+ .auth-wrapper {
184
+ display: flex;
185
+ justify-content: center;
186
+ align-items: center;
187
+ min-height: 100vh;
188
+ background: var(--bg);
189
+ padding: 2rem;
190
+ }
191
+ .auth-card {
192
+ width: 100%;
193
+ max-width: 420px;
194
+ background: var(--card);
195
+ border: 1px solid var(--border);
196
+ border-radius: 12px;
197
+ padding: 2rem;
198
+ box-shadow: 0 4px 16px rgba(0,0,0,0.08);
199
+ }
200
+ .auth-card h2 {
201
+ font-size: 1.3rem;
202
+ font-weight: 700;
203
+ color: var(--text);
204
+ margin-bottom: 1.5rem;
205
+ padding-bottom: 0.75rem;
206
+ border-bottom: 1px solid var(--border);
207
+ }
208
+ .form-group {
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: 0.3rem;
212
+ margin-bottom: 1rem;
213
+ }
214
+ .form-group label {
215
+ font-size: 0.85rem;
216
+ font-weight: 500;
217
+ color: var(--text);
218
+ }
219
+ .form-group small { color: var(--text-muted); font-size: 0.78rem; }
220
+ .form-error { color: var(--danger); font-size: 0.8rem; margin-top: 0.1rem; }
221
+ .auth-link { text-align: center; margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted); }
222
+ .auth-link a { color: var(--primary); font-weight: 500; }
223
+
224
+ /* ===== КНОПКИ ФОРМ ===== */
225
+ .btn-full { width: 100%; margin-top: 0.5rem; }
226
+ .btn-full .p-button { width: 100%; justify-content: center; }
227
+ .btn-row { display: flex; gap: 0.75rem; margin-top: 0.5rem; }
228
+ .btn-half { flex: 1; }
229
+ .btn-half .p-button { width: 100%; justify-content: center; }
230
+
231
+ /* Радиокнопки */
232
+ .radio-group { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.25rem; }
233
+ .radio-option { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
234
+ .radio-option label { cursor: pointer; font-size: 0.9rem; }
235
+ .p-radiobutton .p-radiobutton-box {
236
+ border: 2px solid #cbd5e1 !important;
237
+ background: #fff !important;
238
+ width: 18px !important; height: 18px !important;
239
+ border-radius: 50% !important;
240
+ }
241
+ .p-radiobutton.p-radiobutton-checked .p-radiobutton-box {
242
+ border-color: var(--primary) !important;
243
+ background: #fff !important;
244
+ }
245
+ .p-radiobutton.p-radiobutton-checked .p-radiobutton-box .p-radiobutton-icon {
246
+ background: var(--primary) !important;
247
+ width: 8px !important; height: 8px !important;
248
+ border-radius: 50% !important;
249
+ }
250
+
251
+ /* ===== ADMIN ===== */
252
+ .admin-status-row {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.75rem;
256
+ margin-top: 0.75rem;
257
+ padding-top: 0.75rem;
258
+ border-top: 1px solid var(--border);
259
+ }
260
+ .admin-status-row label { font-size: 0.85rem; font-weight: 500; color: var(--text-muted); white-space: nowrap; }
261
+
262
+ /* ===== DATE PICKER ===== */
263
+ .p-datepicker { width: 100%; }
264
+ .p-datepicker-panel {
265
+ border: 1px solid var(--border) !important;
266
+ border-radius: 8px !important;
267
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
268
+ background: #fff !important;
269
+ }
270
+ .p-datepicker-panel .p-datepicker-day-selected span {
271
+ background: var(--primary) !important;
272
+ color: #fff !important;
273
+ border-radius: 6px !important;
274
+ }
275
+ .p-datepicker-panel .p-datepicker-day:hover span {
276
+ background: var(--primary-light) !important;
277
+ border-radius: 6px !important;
278
+ }
279
+
280
+ /* ===== SELECT ===== */
281
+ .p-select {
282
+ border: 1px solid var(--border) !important;
283
+ border-radius: 6px !important;
284
+ background: #fff !important;
285
+ color: var(--text) !important;
286
+ }
287
+ .p-select .p-select-label { color: var(--text) !important; background: #fff !important; }
288
+ .p-select .p-select-dropdown { background: #fff !important; color: var(--text) !important; }
289
+ .p-select.p-focus { border-color: var(--primary) !important; box-shadow: 0 0 0 3px rgba(37,99,235,0.12) !important; }
290
+ .p-select-overlay {
291
+ background: #fff !important;
292
+ border: 1px solid var(--border) !important;
293
+ border-radius: 8px !important;
294
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
295
+ }
296
+ .p-select-list { background: #fff !important; padding: 0.3rem !important; }
297
+ .p-select-option { background: #fff !important; color: var(--text) !important; border-radius: 5px !important; font-size: 0.9rem !important; }
298
+ .p-select-option:hover { background: var(--primary-light) !important; color: var(--primary) !important; }
299
+ .p-select-option.p-select-option-selected { background: var(--primary) !important; color: #fff !important; }
300
+
301
+ /* ===== DIALOG ===== */
302
+ .p-dialog {
303
+ border: 1px solid var(--border) !important;
304
+ border-radius: 12px !important;
305
+ box-shadow: 0 8px 30px rgba(0,0,0,0.12) !important;
306
+ background: #fff !important;
307
+ }
308
+ .p-dialog .p-dialog-header {
309
+ border-bottom: 1px solid var(--border) !important;
310
+ font-weight: 600 !important;
311
+ color: var(--text) !important;
312
+ background: #fff !important;
313
+ border-radius: 12px 12px 0 0 !important;
314
+ }
315
+ .p-dialog .p-dialog-content { background: #fff !important; color: var(--text) !important; padding: 1.25rem !important; }
316
+ .p-dialog .p-dialog-footer { background: #fff !important; border-top: 1px solid var(--border) !important; border-radius: 0 0 12px 12px !important; }
317
+ .p-dialog-mask { background: rgba(15, 23, 42, 0.4) !important; }
318
+ .p-dialog-header-close-button {
319
+ background: #fff !important; border: 1px solid var(--border) !important;
320
+ border-radius: 6px !important; color: var(--text-muted) !important;
321
+ }
322
+ .p-dialog-header-close-button:hover { background: #f8fafc !important; }
323
+ .p-dialog textarea { background: #fff !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 6px !important; }
324
+
325
+ /* ===== TOAST ===== */
326
+ .p-toast .p-toast-message {
327
+ border-radius: 8px !important;
328
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
329
+ border: none !important;
330
+ }
331
+ .p-toast .p-toast-message.p-toast-message-success { background: #f0fdf4 !important; color: #166534 !important; border-left: 4px solid #16a34a !important; }
332
+ .p-toast .p-toast-message.p-toast-message-error { background: #fef2f2 !important; color: #991b1b !important; border-left: 4px solid #dc2626 !important; }
333
+
334
+ /* ===== MESSAGE (inline) ===== */
335
+ .p-message.p-message-error {
336
+ background: #fef2f2 !important; border: 1px solid #fecaca !important;
337
+ color: var(--danger) !important; border-radius: 6px !important;
338
+ }
339
+
340
+ /* ===== УТИЛИТЫ ===== */
341
+ .flex { display: flex; }
342
+ .flex-col { flex-direction: column; }
343
+ .flex-1 { flex: 1; }
344
+ .flex-wrap { flex-wrap: wrap; }
345
+ .items-center { align-items: center; }
346
+ .items-start { align-items: flex-start; }
347
+ .justify-center { justify-content: center; }
348
+ .justify-between { justify-content: space-between; }
349
+ .justify-end { justify-content: flex-end; }
350
+ .min-h-screen { min-height: 100vh; }
351
+ .min-w-48 { min-width: 12rem; }
352
+ .w-full { width: 100%; }
353
+ .gap-2 { gap: 0.5rem; }
354
+ .gap-3 { gap: 0.75rem; }
355
+ .mt-3 { margin-top: 0.75rem; }
356
+ .text-center { text-align: center; }
357
+ .text-red-500 { color: var(--danger); }
358
+ .text-gray-500 { color: var(--text-muted); }