@noatgnu/cupcake-core 1.0.0 → 1.2.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/fesm2022/noatgnu-cupcake-core.mjs +776 -197
- package/fesm2022/noatgnu-cupcake-core.mjs.map +1 -1
- package/index.d.ts +186 -50
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Injectable, signal, computed, Component, effect, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable, signal, computed, Component, effect, ChangeDetectorRef, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common/http';
|
|
4
4
|
import { HttpClient, HttpParams, provideHttpClient, withInterceptors, HttpClientModule } from '@angular/common/http';
|
|
5
|
-
import { BehaviorSubject, map, tap, throwError, catchError, take, switchMap, filter, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
6
|
-
import { map as map$1 } from 'rxjs/operators';
|
|
5
|
+
import { BehaviorSubject, map, tap, throwError, catchError, Subject, timer, EMPTY, take, switchMap as switchMap$1, filter, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
6
|
+
import { map as map$1, takeUntil, tap as tap$1, switchMap } from 'rxjs/operators';
|
|
7
7
|
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
|
|
8
8
|
import * as i1$1 from '@angular/forms';
|
|
9
9
|
import { FormBuilder, Validators, ReactiveFormsModule, FormsModule, NonNullableFormBuilder } from '@angular/forms';
|
|
@@ -117,37 +117,26 @@ class AuthService {
|
|
|
117
117
|
const refreshToken = this.getRefreshToken();
|
|
118
118
|
if (token && !this.isTokenExpired(token)) {
|
|
119
119
|
this.isAuthenticatedSubject.next(true);
|
|
120
|
-
// Try to get basic user info from token first (for immediate display)
|
|
121
120
|
const user = this.getUserFromToken();
|
|
122
121
|
if (user) {
|
|
123
122
|
this.currentUserSubject.next(user);
|
|
124
123
|
}
|
|
125
|
-
// Then fetch complete user profile from backend to ensure we have latest data
|
|
126
124
|
this.fetchUserProfile().subscribe({
|
|
127
125
|
next: (fullUser) => {
|
|
128
|
-
// Profile fetched successfully
|
|
129
126
|
},
|
|
130
127
|
error: (error) => {
|
|
131
|
-
// Don't clear auth state on profile fetch failure - user might still be authenticated
|
|
132
128
|
}
|
|
133
129
|
});
|
|
134
130
|
}
|
|
135
131
|
else if (refreshToken && token && this.isTokenExpired(token)) {
|
|
136
|
-
// Token is expired but we have refresh token
|
|
137
|
-
// Don't try to refresh here - let the interceptor handle it when needed
|
|
138
|
-
// Just clear the auth state for now but keep the tokens
|
|
139
132
|
this.isAuthenticatedSubject.next(false);
|
|
140
133
|
this.currentUserSubject.next(null);
|
|
141
134
|
}
|
|
142
135
|
else if (refreshToken && !token) {
|
|
143
|
-
// We have refresh token but no access token
|
|
144
|
-
// Keep the refresh token, clear auth state
|
|
145
136
|
this.isAuthenticatedSubject.next(false);
|
|
146
137
|
this.currentUserSubject.next(null);
|
|
147
138
|
}
|
|
148
139
|
else {
|
|
149
|
-
// No tokens at all, just set auth state to false without clearing
|
|
150
|
-
// (tokens might be missing for other reasons, let auth guard handle redirect)
|
|
151
140
|
this.isAuthenticatedSubject.next(false);
|
|
152
141
|
this.currentUserSubject.next(null);
|
|
153
142
|
}
|
|
@@ -267,22 +256,17 @@ class AuthService {
|
|
|
267
256
|
}).pipe(tap((response) => {
|
|
268
257
|
localStorage.setItem('ccvAccessToken', response.access);
|
|
269
258
|
this.isAuthenticatedSubject.next(true);
|
|
270
|
-
// Try to get basic user info from new token first (for immediate display)
|
|
271
259
|
const user = this.getUserFromToken();
|
|
272
260
|
if (user) {
|
|
273
261
|
this.currentUserSubject.next(user);
|
|
274
262
|
}
|
|
275
|
-
// Fetch complete user profile from backend after successful token refresh
|
|
276
263
|
this.fetchUserProfile().subscribe({
|
|
277
264
|
next: (fullUser) => {
|
|
278
|
-
// Profile fetched successfully
|
|
279
265
|
},
|
|
280
266
|
error: (error) => {
|
|
281
|
-
// Failed to fetch user profile after token refresh
|
|
282
267
|
}
|
|
283
268
|
});
|
|
284
269
|
}), catchError((error) => {
|
|
285
|
-
// Refresh failed, clear all auth data
|
|
286
270
|
this.clearAuthData();
|
|
287
271
|
return throwError(() => error);
|
|
288
272
|
}));
|
|
@@ -297,13 +281,10 @@ class AuthService {
|
|
|
297
281
|
}).pipe(tap(response => {
|
|
298
282
|
localStorage.setItem('ccvAccessToken', response.access);
|
|
299
283
|
this.isAuthenticatedSubject.next(true);
|
|
300
|
-
// Fetch complete user profile from backend after successful token refresh
|
|
301
284
|
this.fetchUserProfile().subscribe({
|
|
302
285
|
next: (fullUser) => {
|
|
303
|
-
// Profile fetched successfully
|
|
304
286
|
},
|
|
305
287
|
error: (error) => {
|
|
306
|
-
// Failed to fetch user profile after token refresh
|
|
307
288
|
}
|
|
308
289
|
});
|
|
309
290
|
}));
|
|
@@ -312,18 +293,14 @@ class AuthService {
|
|
|
312
293
|
const token = this.getAccessToken();
|
|
313
294
|
if (token && !this.isTokenExpired(token)) {
|
|
314
295
|
this.isAuthenticatedSubject.next(true);
|
|
315
|
-
// Try to get basic user info from new token first (for immediate display)
|
|
316
296
|
const user = this.getUserFromToken();
|
|
317
297
|
if (user) {
|
|
318
298
|
this.currentUserSubject.next(user);
|
|
319
299
|
}
|
|
320
|
-
// Fetch complete user profile from backend after token refresh
|
|
321
300
|
this.fetchUserProfile().subscribe({
|
|
322
301
|
next: (fullUser) => {
|
|
323
|
-
// Profile fetched successfully
|
|
324
302
|
},
|
|
325
303
|
error: (error) => {
|
|
326
|
-
// Don't clear auth state on profile fetch failure - token refresh was successful
|
|
327
304
|
}
|
|
328
305
|
});
|
|
329
306
|
}
|
|
@@ -334,10 +311,10 @@ class AuthService {
|
|
|
334
311
|
this.currentUserSubject.next(null);
|
|
335
312
|
this.isAuthenticatedSubject.next(false);
|
|
336
313
|
}
|
|
337
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
338
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
314
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
315
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AuthService, providedIn: 'root' });
|
|
339
316
|
}
|
|
340
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
317
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AuthService, decorators: [{
|
|
341
318
|
type: Injectable,
|
|
342
319
|
args: [{
|
|
343
320
|
providedIn: 'root'
|
|
@@ -449,9 +426,9 @@ class ResourceService {
|
|
|
449
426
|
transformed.ownerUsername = legacyData.creator_username;
|
|
450
427
|
delete transformed.creator_username;
|
|
451
428
|
}
|
|
452
|
-
if (legacyData.
|
|
453
|
-
transformed.visibility = this.convertLegacyVisibility(legacyData.
|
|
454
|
-
delete transformed.
|
|
429
|
+
if (legacyData.isPublic !== undefined) {
|
|
430
|
+
transformed.visibility = this.convertLegacyVisibility(legacyData.isPublic, legacyData.isDefault);
|
|
431
|
+
delete transformed.isPublic;
|
|
455
432
|
}
|
|
456
433
|
if (transformed.visibility === undefined) {
|
|
457
434
|
transformed.visibility = ResourceVisibility.PRIVATE;
|
|
@@ -475,15 +452,15 @@ class ResourceService {
|
|
|
475
452
|
delete prepared.ownerUsername;
|
|
476
453
|
}
|
|
477
454
|
if (prepared.visibility !== undefined) {
|
|
478
|
-
prepared.
|
|
455
|
+
prepared.isPublic = this.convertToLegacyVisibility(prepared.visibility);
|
|
479
456
|
delete prepared.visibility;
|
|
480
457
|
}
|
|
481
458
|
return prepared;
|
|
482
459
|
}
|
|
483
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
484
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
460
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResourceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
461
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResourceService, providedIn: 'root' });
|
|
485
462
|
}
|
|
486
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
463
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResourceService, decorators: [{
|
|
487
464
|
type: Injectable,
|
|
488
465
|
args: [{
|
|
489
466
|
providedIn: 'root'
|
|
@@ -824,10 +801,10 @@ class ApiService {
|
|
|
824
801
|
testRemoteHostConnection(id) {
|
|
825
802
|
return this.http.post(`${this.apiUrl}/remote-hosts/${id}/test_connection/`, {});
|
|
826
803
|
}
|
|
827
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
828
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
804
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
805
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ApiService, providedIn: 'root' });
|
|
829
806
|
}
|
|
830
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
807
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ApiService, decorators: [{
|
|
831
808
|
type: Injectable,
|
|
832
809
|
args: [{
|
|
833
810
|
providedIn: 'root'
|
|
@@ -850,12 +827,15 @@ class BaseApiService {
|
|
|
850
827
|
if (obj === null || typeof obj !== 'object') {
|
|
851
828
|
return obj;
|
|
852
829
|
}
|
|
830
|
+
// Skip FormData objects - they should not be transformed
|
|
831
|
+
if (obj instanceof FormData) {
|
|
832
|
+
return obj;
|
|
833
|
+
}
|
|
853
834
|
if (Array.isArray(obj)) {
|
|
854
835
|
return obj.map(item => this.transformToSnakeCase(item));
|
|
855
836
|
}
|
|
856
837
|
const transformed = {};
|
|
857
838
|
Object.entries(obj).forEach(([key, value]) => {
|
|
858
|
-
// Convert camelCase to snake_case
|
|
859
839
|
const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
860
840
|
transformed[snakeKey] = this.transformToSnakeCase(value);
|
|
861
841
|
});
|
|
@@ -873,7 +853,6 @@ class BaseApiService {
|
|
|
873
853
|
}
|
|
874
854
|
const transformed = {};
|
|
875
855
|
Object.entries(obj).forEach(([key, value]) => {
|
|
876
|
-
// Convert snake_case to camelCase
|
|
877
856
|
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
878
857
|
transformed[camelKey] = this.transformToCamelCase(value);
|
|
879
858
|
});
|
|
@@ -919,7 +898,6 @@ class BaseApiService {
|
|
|
919
898
|
buildHttpParams(params) {
|
|
920
899
|
let httpParams = new HttpParams();
|
|
921
900
|
if (params) {
|
|
922
|
-
// Transform to snake_case before creating params
|
|
923
901
|
const transformedParams = this.transformToSnakeCase(params);
|
|
924
902
|
Object.entries(transformedParams).forEach(([key, value]) => {
|
|
925
903
|
if (value !== undefined && value !== null) {
|
|
@@ -929,16 +907,139 @@ class BaseApiService {
|
|
|
929
907
|
}
|
|
930
908
|
return httpParams;
|
|
931
909
|
}
|
|
932
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
933
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
910
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
911
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApiService, providedIn: 'root' });
|
|
934
912
|
}
|
|
935
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
913
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApiService, decorators: [{
|
|
936
914
|
type: Injectable,
|
|
937
915
|
args: [{
|
|
938
916
|
providedIn: 'root'
|
|
939
917
|
}]
|
|
940
918
|
}] });
|
|
941
919
|
|
|
920
|
+
class ToastService {
|
|
921
|
+
toastsSignal = signal([], ...(ngDevMode ? [{ debugName: "toastsSignal" }] : []));
|
|
922
|
+
toasts = this.toastsSignal.asReadonly();
|
|
923
|
+
show(message, type = 'info', duration = 5000) {
|
|
924
|
+
const id = this.generateId();
|
|
925
|
+
const toast = {
|
|
926
|
+
id,
|
|
927
|
+
message,
|
|
928
|
+
type,
|
|
929
|
+
duration,
|
|
930
|
+
dismissible: true
|
|
931
|
+
};
|
|
932
|
+
this.toastsSignal.update(toasts => [...toasts, toast]);
|
|
933
|
+
if (duration > 0) {
|
|
934
|
+
setTimeout(() => {
|
|
935
|
+
this.remove(id);
|
|
936
|
+
}, duration);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
success(message, duration = 5000) {
|
|
940
|
+
this.show(message, 'success', duration);
|
|
941
|
+
}
|
|
942
|
+
error(message, duration = 8000) {
|
|
943
|
+
this.show(message, 'error', duration);
|
|
944
|
+
}
|
|
945
|
+
warning(message, duration = 6000) {
|
|
946
|
+
this.show(message, 'warning', duration);
|
|
947
|
+
}
|
|
948
|
+
info(message, duration = 5000) {
|
|
949
|
+
this.show(message, 'info', duration);
|
|
950
|
+
}
|
|
951
|
+
remove(id) {
|
|
952
|
+
this.toastsSignal.update(toasts => toasts.filter(toast => toast.id !== id));
|
|
953
|
+
}
|
|
954
|
+
clear() {
|
|
955
|
+
this.toastsSignal.set([]);
|
|
956
|
+
}
|
|
957
|
+
generateId() {
|
|
958
|
+
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
959
|
+
}
|
|
960
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
961
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ToastService, providedIn: 'root' });
|
|
962
|
+
}
|
|
963
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ToastService, decorators: [{
|
|
964
|
+
type: Injectable,
|
|
965
|
+
args: [{
|
|
966
|
+
providedIn: 'root'
|
|
967
|
+
}]
|
|
968
|
+
}] });
|
|
969
|
+
|
|
970
|
+
class NotificationService {
|
|
971
|
+
toastService;
|
|
972
|
+
notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
|
|
973
|
+
notificationSubject = new Subject();
|
|
974
|
+
allNotifications = computed(() => this.notifications(), ...(ngDevMode ? [{ debugName: "allNotifications" }] : []));
|
|
975
|
+
unreadNotifications = computed(() => this.notifications().filter(n => !n.read), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : []));
|
|
976
|
+
unreadCount = computed(() => this.unreadNotifications().length, ...(ngDevMode ? [{ debugName: "unreadCount" }] : []));
|
|
977
|
+
notification$ = this.notificationSubject.asObservable();
|
|
978
|
+
constructor(toastService) {
|
|
979
|
+
this.toastService = toastService;
|
|
980
|
+
}
|
|
981
|
+
addNotification(notification) {
|
|
982
|
+
this.notifications.update(notifications => {
|
|
983
|
+
const updated = [notification, ...notifications];
|
|
984
|
+
return updated.slice(0, 100);
|
|
985
|
+
});
|
|
986
|
+
this.notificationSubject.next(notification);
|
|
987
|
+
console.log('Notification added:', notification);
|
|
988
|
+
}
|
|
989
|
+
generateNotificationId() {
|
|
990
|
+
return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
991
|
+
}
|
|
992
|
+
markAsRead(notificationId) {
|
|
993
|
+
this.notifications.update(notifications => notifications.map(n => n.id === notificationId ? { ...n, read: true } : n));
|
|
994
|
+
}
|
|
995
|
+
markAllAsRead() {
|
|
996
|
+
this.notifications.update(notifications => notifications.map(n => ({ ...n, read: true })));
|
|
997
|
+
}
|
|
998
|
+
removeNotification(notificationId) {
|
|
999
|
+
this.notifications.update(notifications => notifications.filter(n => n.id !== notificationId));
|
|
1000
|
+
}
|
|
1001
|
+
clearAll() {
|
|
1002
|
+
this.notifications.set([]);
|
|
1003
|
+
}
|
|
1004
|
+
clearRead() {
|
|
1005
|
+
this.notifications.update(notifications => notifications.filter(n => !n.read));
|
|
1006
|
+
}
|
|
1007
|
+
createLocalNotification(type, title, message, level = 'info', autoClose = true) {
|
|
1008
|
+
const notification = {
|
|
1009
|
+
id: this.generateNotificationId(),
|
|
1010
|
+
type,
|
|
1011
|
+
title,
|
|
1012
|
+
message,
|
|
1013
|
+
timestamp: new Date(),
|
|
1014
|
+
read: false,
|
|
1015
|
+
level,
|
|
1016
|
+
autoClose
|
|
1017
|
+
};
|
|
1018
|
+
this.addNotification(notification);
|
|
1019
|
+
switch (level) {
|
|
1020
|
+
case 'error':
|
|
1021
|
+
this.toastService.error(`${title}: ${message}`);
|
|
1022
|
+
break;
|
|
1023
|
+
case 'warning':
|
|
1024
|
+
this.toastService.warning(`${title}: ${message}`);
|
|
1025
|
+
break;
|
|
1026
|
+
case 'success':
|
|
1027
|
+
this.toastService.success(`${title}: ${message}`);
|
|
1028
|
+
break;
|
|
1029
|
+
default:
|
|
1030
|
+
this.toastService.info(`${title}: ${message}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NotificationService, deps: [{ token: ToastService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1034
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NotificationService, providedIn: 'root' });
|
|
1035
|
+
}
|
|
1036
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NotificationService, decorators: [{
|
|
1037
|
+
type: Injectable,
|
|
1038
|
+
args: [{
|
|
1039
|
+
providedIn: 'root'
|
|
1040
|
+
}]
|
|
1041
|
+
}], ctorParameters: () => [{ type: ToastService }] });
|
|
1042
|
+
|
|
942
1043
|
class SiteConfigService extends BaseApiService {
|
|
943
1044
|
defaultConfig = {
|
|
944
1045
|
siteName: 'CUPCAKE Vanilla',
|
|
@@ -1005,67 +1106,89 @@ class SiteConfigService extends BaseApiService {
|
|
|
1005
1106
|
isOrcidLoginEnabled() {
|
|
1006
1107
|
return this.configSubject.value.enableOrcidLogin === true;
|
|
1007
1108
|
}
|
|
1008
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1009
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1109
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SiteConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1110
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
|
|
1010
1111
|
}
|
|
1011
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1112
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SiteConfigService, decorators: [{
|
|
1012
1113
|
type: Injectable,
|
|
1013
1114
|
args: [{
|
|
1014
1115
|
providedIn: 'root'
|
|
1015
1116
|
}]
|
|
1016
1117
|
}], ctorParameters: () => [] });
|
|
1017
1118
|
|
|
1018
|
-
class
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
this.
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1119
|
+
class ThemeService {
|
|
1120
|
+
THEME_KEY = 'cupcake-theme';
|
|
1121
|
+
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1122
|
+
_theme = signal(this.loadStoredTheme(), ...(ngDevMode ? [{ debugName: "_theme" }] : []));
|
|
1123
|
+
theme = this._theme.asReadonly();
|
|
1124
|
+
isDark = computed(() => {
|
|
1125
|
+
const theme = this._theme();
|
|
1126
|
+
if (theme === 'auto') {
|
|
1127
|
+
return this.mediaQuery.matches;
|
|
1128
|
+
}
|
|
1129
|
+
return theme === 'dark';
|
|
1130
|
+
}, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
|
|
1131
|
+
constructor() {
|
|
1132
|
+
this.mediaQuery.addEventListener('change', () => {
|
|
1133
|
+
if (this._theme() === 'auto') {
|
|
1134
|
+
this.updateDocumentTheme();
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
this.updateDocumentTheme();
|
|
1038
1138
|
}
|
|
1039
|
-
|
|
1040
|
-
this.
|
|
1139
|
+
setTheme(theme) {
|
|
1140
|
+
this._theme.set(theme);
|
|
1141
|
+
localStorage.setItem(this.THEME_KEY, theme);
|
|
1142
|
+
this.updateDocumentTheme();
|
|
1041
1143
|
}
|
|
1042
|
-
|
|
1043
|
-
this.
|
|
1144
|
+
toggleTheme() {
|
|
1145
|
+
const current = this._theme();
|
|
1146
|
+
if (current === 'light') {
|
|
1147
|
+
this.setTheme('dark');
|
|
1148
|
+
}
|
|
1149
|
+
else if (current === 'dark') {
|
|
1150
|
+
this.setTheme('auto');
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
this.setTheme('light');
|
|
1154
|
+
}
|
|
1044
1155
|
}
|
|
1045
|
-
|
|
1046
|
-
this.
|
|
1156
|
+
loadStoredTheme() {
|
|
1157
|
+
const stored = localStorage.getItem(this.THEME_KEY);
|
|
1158
|
+
return stored && ['light', 'dark', 'auto'].includes(stored) ? stored : 'auto';
|
|
1047
1159
|
}
|
|
1048
|
-
|
|
1049
|
-
this.
|
|
1160
|
+
updateDocumentTheme() {
|
|
1161
|
+
const isDark = this.isDark();
|
|
1162
|
+
document.documentElement.setAttribute('data-bs-theme', isDark ? 'dark' : 'light');
|
|
1163
|
+
document.documentElement.classList.toggle('dark-mode', isDark);
|
|
1050
1164
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1165
|
+
getThemeIcon() {
|
|
1166
|
+
const theme = this._theme();
|
|
1167
|
+
switch (theme) {
|
|
1168
|
+
case 'light': return 'bi-sun-fill';
|
|
1169
|
+
case 'dark': return 'bi-moon-fill';
|
|
1170
|
+
case 'auto': return 'bi-circle-half';
|
|
1171
|
+
default: return 'bi-circle-half';
|
|
1172
|
+
}
|
|
1056
1173
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1174
|
+
getThemeLabel() {
|
|
1175
|
+
const theme = this._theme();
|
|
1176
|
+
switch (theme) {
|
|
1177
|
+
case 'light': return 'Light Mode';
|
|
1178
|
+
case 'dark': return 'Dark Mode';
|
|
1179
|
+
case 'auto': return 'Auto Mode';
|
|
1180
|
+
default: return 'Auto Mode';
|
|
1181
|
+
}
|
|
1059
1182
|
}
|
|
1060
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1061
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1183
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1184
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
1062
1185
|
}
|
|
1063
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1186
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ThemeService, decorators: [{
|
|
1064
1187
|
type: Injectable,
|
|
1065
1188
|
args: [{
|
|
1066
1189
|
providedIn: 'root'
|
|
1067
1190
|
}]
|
|
1068
|
-
}] });
|
|
1191
|
+
}], ctorParameters: () => [] });
|
|
1069
1192
|
|
|
1070
1193
|
class UserManagementService {
|
|
1071
1194
|
apiService = inject(ApiService);
|
|
@@ -1134,10 +1257,398 @@ class UserManagementService {
|
|
|
1134
1257
|
getCurrentTotalUsers() {
|
|
1135
1258
|
return this.totalUsersSubject.getValue();
|
|
1136
1259
|
}
|
|
1137
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1138
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1260
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1261
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserManagementService, providedIn: 'root' });
|
|
1139
1262
|
}
|
|
1140
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1263
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserManagementService, decorators: [{
|
|
1264
|
+
type: Injectable,
|
|
1265
|
+
args: [{
|
|
1266
|
+
providedIn: 'root'
|
|
1267
|
+
}]
|
|
1268
|
+
}], ctorParameters: () => [] });
|
|
1269
|
+
|
|
1270
|
+
const WEBSOCKET_ENDPOINT = new InjectionToken('WEBSOCKET_ENDPOINT', {
|
|
1271
|
+
providedIn: 'root',
|
|
1272
|
+
factory: () => 'notifications'
|
|
1273
|
+
});
|
|
1274
|
+
class WebSocketService {
|
|
1275
|
+
authService;
|
|
1276
|
+
ws = null;
|
|
1277
|
+
config;
|
|
1278
|
+
destroy$ = new Subject();
|
|
1279
|
+
reconnectAttempts = 0;
|
|
1280
|
+
isConnecting = false;
|
|
1281
|
+
connectionState = signal('disconnected', ...(ngDevMode ? [{ debugName: "connectionState" }] : []));
|
|
1282
|
+
lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : []));
|
|
1283
|
+
messageSubject = new Subject();
|
|
1284
|
+
connectionSubject = new BehaviorSubject(false);
|
|
1285
|
+
messages$ = this.messageSubject.asObservable();
|
|
1286
|
+
isConnected$ = this.connectionSubject.asObservable();
|
|
1287
|
+
connectionState$ = computed(() => this.connectionState(), ...(ngDevMode ? [{ debugName: "connectionState$" }] : []));
|
|
1288
|
+
lastError$ = computed(() => this.lastError(), ...(ngDevMode ? [{ debugName: "lastError$" }] : []));
|
|
1289
|
+
config_token = inject(CUPCAKE_CORE_CONFIG);
|
|
1290
|
+
endpoint = inject(WEBSOCKET_ENDPOINT, { optional: true }) || 'notifications';
|
|
1291
|
+
constructor(authService) {
|
|
1292
|
+
this.authService = authService;
|
|
1293
|
+
this.config = {
|
|
1294
|
+
url: this.getWebSocketUrl(),
|
|
1295
|
+
endpoint: this.endpoint,
|
|
1296
|
+
reconnectInterval: this.getAdaptiveReconnectInterval(),
|
|
1297
|
+
maxReconnectAttempts: 3
|
|
1298
|
+
};
|
|
1299
|
+
this.authService.isAuthenticated$.subscribe(isAuthenticated => {
|
|
1300
|
+
if (!isAuthenticated && this.ws) {
|
|
1301
|
+
console.log('User logged out - disconnecting WebSocket');
|
|
1302
|
+
this.disconnect();
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
this.setupBrowserResourceHandling();
|
|
1306
|
+
}
|
|
1307
|
+
getWebSocketUrl() {
|
|
1308
|
+
const apiUrl = this.config_token.apiUrl;
|
|
1309
|
+
try {
|
|
1310
|
+
const url = new URL(apiUrl);
|
|
1311
|
+
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1312
|
+
const host = url.host;
|
|
1313
|
+
const endpoint = this.config?.endpoint || 'notifications';
|
|
1314
|
+
return `${protocol}//${host}/ws/${endpoint}/`;
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
console.error('Invalid API URL in cupcake config:', apiUrl);
|
|
1318
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1319
|
+
const host = window.location.host;
|
|
1320
|
+
const endpoint = this.config?.endpoint || 'notifications';
|
|
1321
|
+
return `${protocol}//${host}/ws/${endpoint}/`;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
connect() {
|
|
1325
|
+
console.log('🔌 WebSocket connect() called');
|
|
1326
|
+
console.log('🔌 Current connection state:', this.connectionState());
|
|
1327
|
+
console.log('🔌 WebSocket URL:', this.config.url);
|
|
1328
|
+
if (this.isConnecting) {
|
|
1329
|
+
console.log('WebSocket connection already in progress');
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1333
|
+
console.log('WebSocket already connected');
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
|
|
1337
|
+
console.log('Closing existing WebSocket connection');
|
|
1338
|
+
this.ws.close();
|
|
1339
|
+
this.ws = null;
|
|
1340
|
+
}
|
|
1341
|
+
const token = this.authService.getAccessToken();
|
|
1342
|
+
if (!token) {
|
|
1343
|
+
console.error('❌ Cannot connect WebSocket - no authentication token');
|
|
1344
|
+
this.lastError.set('Authentication required');
|
|
1345
|
+
this.connectionState.set('error');
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
this.isConnecting = true;
|
|
1349
|
+
this.connectionState.set('connecting');
|
|
1350
|
+
this.lastError.set(null);
|
|
1351
|
+
try {
|
|
1352
|
+
const wsUrl = `${this.config.url}?token=${encodeURIComponent(token)}`;
|
|
1353
|
+
console.log('Connecting to WebSocket:', wsUrl.replace(token, '[TOKEN_HIDDEN]'));
|
|
1354
|
+
this.ws = new WebSocket(wsUrl);
|
|
1355
|
+
this.ws.onopen = this.onOpen.bind(this);
|
|
1356
|
+
this.ws.onmessage = this.onMessage.bind(this);
|
|
1357
|
+
this.ws.onerror = this.onError.bind(this);
|
|
1358
|
+
this.ws.onclose = this.onClose.bind(this);
|
|
1359
|
+
const connectionTimeout = setTimeout(() => {
|
|
1360
|
+
if (this.ws?.readyState === WebSocket.CONNECTING) {
|
|
1361
|
+
console.warn('WebSocket connection timeout - closing');
|
|
1362
|
+
this.ws.close();
|
|
1363
|
+
this.lastError.set('Connection timeout');
|
|
1364
|
+
this.connectionState.set('error');
|
|
1365
|
+
this.isConnecting = false;
|
|
1366
|
+
}
|
|
1367
|
+
}, 10000);
|
|
1368
|
+
this.ws.onopen = (event) => {
|
|
1369
|
+
clearTimeout(connectionTimeout);
|
|
1370
|
+
this.onOpen(event);
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
console.error('WebSocket connection error:', error);
|
|
1375
|
+
this.connectionState.set('error');
|
|
1376
|
+
this.lastError.set('Connection failed');
|
|
1377
|
+
this.isConnecting = false;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
disconnect() {
|
|
1381
|
+
this.destroy$.next();
|
|
1382
|
+
this.isConnecting = false;
|
|
1383
|
+
if (this.ws) {
|
|
1384
|
+
this.ws.close(1000, 'User disconnected');
|
|
1385
|
+
this.ws = null;
|
|
1386
|
+
}
|
|
1387
|
+
this.connectionState.set('disconnected');
|
|
1388
|
+
this.connectionSubject.next(false);
|
|
1389
|
+
this.reconnectAttempts = 0;
|
|
1390
|
+
}
|
|
1391
|
+
send(message) {
|
|
1392
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1393
|
+
this.ws.send(JSON.stringify(message));
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
console.warn('WebSocket not connected, cannot send message:', message);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
subscribe(subscriptionType, options = {}) {
|
|
1400
|
+
this.send({
|
|
1401
|
+
type: 'subscribe',
|
|
1402
|
+
subscription_type: subscriptionType,
|
|
1403
|
+
...options
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
onOpen(event) {
|
|
1407
|
+
console.log('WebSocket connected');
|
|
1408
|
+
this.isConnecting = false;
|
|
1409
|
+
this.connectionState.set('connected');
|
|
1410
|
+
this.connectionSubject.next(true);
|
|
1411
|
+
this.reconnectAttempts = 0;
|
|
1412
|
+
this.lastError.set(null);
|
|
1413
|
+
}
|
|
1414
|
+
onMessage(event) {
|
|
1415
|
+
try {
|
|
1416
|
+
const data = JSON.parse(event.data);
|
|
1417
|
+
console.log('WebSocket message received:', data);
|
|
1418
|
+
this.messageSubject.next(data);
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
console.error('Error parsing WebSocket message:', error);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
onError(event) {
|
|
1425
|
+
console.error('WebSocket error:', event);
|
|
1426
|
+
this.isConnecting = false;
|
|
1427
|
+
this.connectionState.set('error');
|
|
1428
|
+
this.lastError.set('Connection error occurred');
|
|
1429
|
+
}
|
|
1430
|
+
onClose(event) {
|
|
1431
|
+
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
|
|
1432
|
+
this.isConnecting = false;
|
|
1433
|
+
this.connectionState.set('disconnected');
|
|
1434
|
+
this.connectionSubject.next(false);
|
|
1435
|
+
if (event.code === 4001) {
|
|
1436
|
+
this.lastError.set('Authentication failed');
|
|
1437
|
+
console.error('WebSocket authentication failed');
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
else if (event.code === 4003) {
|
|
1441
|
+
this.lastError.set('Insufficient permissions');
|
|
1442
|
+
console.error('WebSocket permission denied');
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (event.code !== 1000 && this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
|
|
1446
|
+
this.attemptReconnection();
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
attemptReconnection() {
|
|
1450
|
+
this.reconnectAttempts++;
|
|
1451
|
+
const delay = this.config.reconnectInterval || 5000;
|
|
1452
|
+
console.log(`WebSocket reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
1453
|
+
timer(delay).pipe(takeUntil(this.destroy$), tap$1(() => {
|
|
1454
|
+
if (this.reconnectAttempts <= (this.config.maxReconnectAttempts || 5)) {
|
|
1455
|
+
this.connect();
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
console.error('Max WebSocket reconnection attempts reached');
|
|
1459
|
+
this.lastError.set('Connection failed - max attempts reached');
|
|
1460
|
+
}
|
|
1461
|
+
})).subscribe();
|
|
1462
|
+
}
|
|
1463
|
+
filterMessages(type) {
|
|
1464
|
+
return this.messages$.pipe(tap$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap(message => message.type === type ? [message] : EMPTY));
|
|
1465
|
+
}
|
|
1466
|
+
getNotifications() {
|
|
1467
|
+
return this.filterMessages('notification');
|
|
1468
|
+
}
|
|
1469
|
+
getSystemNotifications() {
|
|
1470
|
+
return this.filterMessages('system.notification');
|
|
1471
|
+
}
|
|
1472
|
+
reconnectWithNewToken() {
|
|
1473
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1474
|
+
console.log('Reconnecting WebSocket with new token');
|
|
1475
|
+
this.disconnect();
|
|
1476
|
+
setTimeout(() => this.connect(), 100);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
shouldConnect() {
|
|
1480
|
+
return this.authService.isAuthenticated() && !!this.authService.getAccessToken();
|
|
1481
|
+
}
|
|
1482
|
+
updateConfig() {
|
|
1483
|
+
this.config.url = this.getWebSocketUrl();
|
|
1484
|
+
console.log('WebSocket URL updated to:', this.config.url);
|
|
1485
|
+
}
|
|
1486
|
+
ngOnDestroy() {
|
|
1487
|
+
this.disconnect();
|
|
1488
|
+
}
|
|
1489
|
+
getAdaptiveReconnectInterval() {
|
|
1490
|
+
const baseInterval = 5000;
|
|
1491
|
+
const tabCount = this.estimateTabCount();
|
|
1492
|
+
if (tabCount > 20) {
|
|
1493
|
+
return baseInterval * 3;
|
|
1494
|
+
}
|
|
1495
|
+
else if (tabCount > 10) {
|
|
1496
|
+
return baseInterval * 2;
|
|
1497
|
+
}
|
|
1498
|
+
return baseInterval;
|
|
1499
|
+
}
|
|
1500
|
+
estimateTabCount() {
|
|
1501
|
+
try {
|
|
1502
|
+
if ('memory' in performance) {
|
|
1503
|
+
const memory = performance.memory;
|
|
1504
|
+
const usedMB = memory.usedJSHeapSize / (1024 * 1024);
|
|
1505
|
+
if (usedMB > 500)
|
|
1506
|
+
return 25;
|
|
1507
|
+
if (usedMB > 300)
|
|
1508
|
+
return 15;
|
|
1509
|
+
if (usedMB > 150)
|
|
1510
|
+
return 10;
|
|
1511
|
+
return 5;
|
|
1512
|
+
}
|
|
1513
|
+
return 10;
|
|
1514
|
+
}
|
|
1515
|
+
catch (error) {
|
|
1516
|
+
return 10;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
canConnectSafely() {
|
|
1520
|
+
try {
|
|
1521
|
+
if ('memory' in performance) {
|
|
1522
|
+
const memory = performance.memory;
|
|
1523
|
+
const usedMB = memory.usedJSHeapSize / (1024 * 1024);
|
|
1524
|
+
const totalMB = memory.totalJSHeapSize / (1024 * 1024);
|
|
1525
|
+
const memoryUsageRatio = usedMB / totalMB;
|
|
1526
|
+
if (memoryUsageRatio > 0.9) {
|
|
1527
|
+
console.warn('High memory usage detected, delaying WebSocket connection');
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
const tabCount = this.estimateTabCount();
|
|
1532
|
+
if (tabCount > 30) {
|
|
1533
|
+
console.warn('Too many tabs detected, may affect WebSocket reliability');
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
catch (error) {
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
setupBrowserResourceHandling() {
|
|
1543
|
+
document.addEventListener('visibilitychange', () => {
|
|
1544
|
+
if (document.visibilityState === 'visible') {
|
|
1545
|
+
if (this.shouldConnect() && this.connectionState() === 'disconnected') {
|
|
1546
|
+
console.log('Tab became active - attempting WebSocket reconnection');
|
|
1547
|
+
setTimeout(() => this.connect(), 1000);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
const tabCount = this.estimateTabCount();
|
|
1552
|
+
if (tabCount > 15 && this.ws?.readyState === WebSocket.OPEN) {
|
|
1553
|
+
console.log('Tab inactive with high tab count - maintaining connection with reduced activity');
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
window.addEventListener('beforeunload', () => {
|
|
1558
|
+
if (this.ws) {
|
|
1559
|
+
this.ws.close(1000, 'Page unloading');
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1564
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketService, providedIn: 'root' });
|
|
1565
|
+
}
|
|
1566
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketService, decorators: [{
|
|
1567
|
+
type: Injectable,
|
|
1568
|
+
args: [{
|
|
1569
|
+
providedIn: 'root'
|
|
1570
|
+
}]
|
|
1571
|
+
}], ctorParameters: () => [{ type: AuthService }] });
|
|
1572
|
+
|
|
1573
|
+
const WEBSOCKET_ENDPOINTS = new InjectionToken('WEBSOCKET_ENDPOINTS');
|
|
1574
|
+
class WebSocketEndpoints {
|
|
1575
|
+
static CORE_NOTIFICATIONS = 'notifications';
|
|
1576
|
+
static CORE_ADMIN = 'admin';
|
|
1577
|
+
static CCV_NOTIFICATIONS = 'ccv/notifications';
|
|
1578
|
+
static CCV_ADMIN = 'ccv/admin';
|
|
1579
|
+
static CCM_NOTIFICATIONS = 'ccm/notifications';
|
|
1580
|
+
static CCRV_NOTIFICATIONS = 'ccrv/notifications';
|
|
1581
|
+
static CCSC_NOTIFICATIONS = 'ccsc/notifications';
|
|
1582
|
+
}
|
|
1583
|
+
class WebSocketConfigService {
|
|
1584
|
+
endpoints = new Map();
|
|
1585
|
+
constructor() {
|
|
1586
|
+
this.registerDefaultEndpoints();
|
|
1587
|
+
}
|
|
1588
|
+
registerDefaultEndpoints() {
|
|
1589
|
+
this.registerEndpoint({
|
|
1590
|
+
app: 'core',
|
|
1591
|
+
endpoint: WebSocketEndpoints.CORE_NOTIFICATIONS,
|
|
1592
|
+
description: 'Core notification endpoint'
|
|
1593
|
+
});
|
|
1594
|
+
this.registerEndpoint({
|
|
1595
|
+
app: 'core',
|
|
1596
|
+
endpoint: WebSocketEndpoints.CORE_ADMIN,
|
|
1597
|
+
description: 'Core admin notification endpoint'
|
|
1598
|
+
});
|
|
1599
|
+
this.registerEndpoint({
|
|
1600
|
+
app: 'ccv',
|
|
1601
|
+
endpoint: WebSocketEndpoints.CCV_NOTIFICATIONS,
|
|
1602
|
+
description: 'CCV notification endpoint'
|
|
1603
|
+
});
|
|
1604
|
+
this.registerEndpoint({
|
|
1605
|
+
app: 'ccv',
|
|
1606
|
+
endpoint: WebSocketEndpoints.CCV_ADMIN,
|
|
1607
|
+
description: 'CCV admin notification endpoint'
|
|
1608
|
+
});
|
|
1609
|
+
this.registerEndpoint({
|
|
1610
|
+
app: 'ccm',
|
|
1611
|
+
endpoint: WebSocketEndpoints.CCM_NOTIFICATIONS,
|
|
1612
|
+
description: 'CCM notification endpoint'
|
|
1613
|
+
});
|
|
1614
|
+
this.registerEndpoint({
|
|
1615
|
+
app: 'ccrv',
|
|
1616
|
+
endpoint: WebSocketEndpoints.CCRV_NOTIFICATIONS,
|
|
1617
|
+
description: 'CCRV notification endpoint'
|
|
1618
|
+
});
|
|
1619
|
+
this.registerEndpoint({
|
|
1620
|
+
app: 'ccsc',
|
|
1621
|
+
endpoint: WebSocketEndpoints.CCSC_NOTIFICATIONS,
|
|
1622
|
+
description: 'CCSC notification endpoint'
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
registerEndpoint(config) {
|
|
1626
|
+
this.endpoints.set(config.endpoint, config);
|
|
1627
|
+
}
|
|
1628
|
+
getEndpoint(endpoint) {
|
|
1629
|
+
return this.endpoints.get(endpoint);
|
|
1630
|
+
}
|
|
1631
|
+
getEndpointsForApp(app) {
|
|
1632
|
+
return Array.from(this.endpoints.values()).filter(config => config.app === app);
|
|
1633
|
+
}
|
|
1634
|
+
getAllEndpoints() {
|
|
1635
|
+
return Array.from(this.endpoints.values());
|
|
1636
|
+
}
|
|
1637
|
+
validateEndpoint(endpoint) {
|
|
1638
|
+
return this.endpoints.has(endpoint);
|
|
1639
|
+
}
|
|
1640
|
+
getDefaultEndpointForApp(app) {
|
|
1641
|
+
const endpoints = this.getEndpointsForApp(app);
|
|
1642
|
+
if (endpoints.length === 0) {
|
|
1643
|
+
return WebSocketEndpoints.CORE_NOTIFICATIONS;
|
|
1644
|
+
}
|
|
1645
|
+
const notificationEndpoint = endpoints.find(e => e.endpoint.includes('notifications'));
|
|
1646
|
+
return notificationEndpoint?.endpoint || endpoints[0].endpoint;
|
|
1647
|
+
}
|
|
1648
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1649
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketConfigService, providedIn: 'root' });
|
|
1650
|
+
}
|
|
1651
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: WebSocketConfigService, decorators: [{
|
|
1141
1652
|
type: Injectable,
|
|
1142
1653
|
args: [{
|
|
1143
1654
|
providedIn: 'root'
|
|
@@ -1192,16 +1703,43 @@ class LabGroupService extends BaseApiService {
|
|
|
1192
1703
|
cancelLabGroupInvitation(id) {
|
|
1193
1704
|
return this.post(`${this.apiUrl}/lab-group-invitations/${id}/cancel_invitation/`, {});
|
|
1194
1705
|
}
|
|
1195
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1196
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1706
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LabGroupService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1707
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LabGroupService, providedIn: 'root' });
|
|
1197
1708
|
}
|
|
1198
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1709
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LabGroupService, decorators: [{
|
|
1199
1710
|
type: Injectable,
|
|
1200
1711
|
args: [{
|
|
1201
1712
|
providedIn: 'root'
|
|
1202
1713
|
}]
|
|
1203
1714
|
}] });
|
|
1204
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* Extract original return URL to prevent accumulating login URLs
|
|
1718
|
+
*/
|
|
1719
|
+
function getCleanReturnUrl$1(currentUrl) {
|
|
1720
|
+
try {
|
|
1721
|
+
const url = new URL(currentUrl, window.location.origin);
|
|
1722
|
+
if (url.pathname === '/login') {
|
|
1723
|
+
const innerReturnUrl = url.searchParams.get('returnUrl');
|
|
1724
|
+
if (innerReturnUrl) {
|
|
1725
|
+
return getCleanReturnUrl$1(innerReturnUrl);
|
|
1726
|
+
}
|
|
1727
|
+
return '/';
|
|
1728
|
+
}
|
|
1729
|
+
return url.pathname + url.search;
|
|
1730
|
+
}
|
|
1731
|
+
catch (error) {
|
|
1732
|
+
if (currentUrl.startsWith('/login')) {
|
|
1733
|
+
const urlParams = new URLSearchParams(currentUrl.split('?')[1]);
|
|
1734
|
+
const innerReturnUrl = urlParams.get('returnUrl');
|
|
1735
|
+
if (innerReturnUrl) {
|
|
1736
|
+
return getCleanReturnUrl$1(innerReturnUrl);
|
|
1737
|
+
}
|
|
1738
|
+
return '/';
|
|
1739
|
+
}
|
|
1740
|
+
return currentUrl;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1205
1743
|
const authGuard = (route, state) => {
|
|
1206
1744
|
const authService = inject(AuthService);
|
|
1207
1745
|
const router = inject(Router);
|
|
@@ -1209,19 +1747,56 @@ const authGuard = (route, state) => {
|
|
|
1209
1747
|
if (isAuthenticated) {
|
|
1210
1748
|
return true;
|
|
1211
1749
|
}
|
|
1750
|
+
const cleanReturnUrl = getCleanReturnUrl$1(state.url);
|
|
1212
1751
|
router.navigate(['/login'], {
|
|
1213
|
-
queryParams: { returnUrl:
|
|
1752
|
+
queryParams: { returnUrl: cleanReturnUrl }
|
|
1214
1753
|
});
|
|
1215
1754
|
return false;
|
|
1216
1755
|
}));
|
|
1217
1756
|
};
|
|
1218
1757
|
|
|
1758
|
+
/**
|
|
1759
|
+
* Extract original return URL to prevent accumulating login URLs
|
|
1760
|
+
*/
|
|
1761
|
+
function getCleanReturnUrl(currentUrl) {
|
|
1762
|
+
try {
|
|
1763
|
+
const url = new URL(currentUrl, window.location.origin);
|
|
1764
|
+
// If current URL is already a login page, extract its returnUrl
|
|
1765
|
+
if (url.pathname === '/login') {
|
|
1766
|
+
const innerReturnUrl = url.searchParams.get('returnUrl');
|
|
1767
|
+
if (innerReturnUrl) {
|
|
1768
|
+
// Recursively clean in case of nested login URLs
|
|
1769
|
+
return getCleanReturnUrl(innerReturnUrl);
|
|
1770
|
+
}
|
|
1771
|
+
// If no returnUrl in login URL, use home page
|
|
1772
|
+
return '/';
|
|
1773
|
+
}
|
|
1774
|
+
// Return clean URL without origin
|
|
1775
|
+
return url.pathname + url.search;
|
|
1776
|
+
}
|
|
1777
|
+
catch (error) {
|
|
1778
|
+
// Fallback for relative URLs
|
|
1779
|
+
if (currentUrl.startsWith('/login')) {
|
|
1780
|
+
const urlParams = new URLSearchParams(currentUrl.split('?')[1]);
|
|
1781
|
+
const innerReturnUrl = urlParams.get('returnUrl');
|
|
1782
|
+
if (innerReturnUrl) {
|
|
1783
|
+
return getCleanReturnUrl(innerReturnUrl);
|
|
1784
|
+
}
|
|
1785
|
+
return '/';
|
|
1786
|
+
}
|
|
1787
|
+
return currentUrl;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1219
1790
|
const adminGuard = (route, state) => {
|
|
1220
1791
|
const authService = inject(AuthService);
|
|
1221
1792
|
const router = inject(Router);
|
|
1222
1793
|
const user = authService.getCurrentUser();
|
|
1223
1794
|
if (!user) {
|
|
1224
|
-
|
|
1795
|
+
// Clean the return URL to prevent login URL accumulation
|
|
1796
|
+
const cleanReturnUrl = getCleanReturnUrl(state.url);
|
|
1797
|
+
router.navigate(['/login'], {
|
|
1798
|
+
queryParams: { returnUrl: cleanReturnUrl }
|
|
1799
|
+
});
|
|
1225
1800
|
return false;
|
|
1226
1801
|
}
|
|
1227
1802
|
if (!user.isStaff) {
|
|
@@ -1231,9 +1806,7 @@ const adminGuard = (route, state) => {
|
|
|
1231
1806
|
return true;
|
|
1232
1807
|
};
|
|
1233
1808
|
|
|
1234
|
-
// Global flag to prevent multiple simultaneous token refresh attempts
|
|
1235
1809
|
let isRefreshing = false;
|
|
1236
|
-
// Subject to coordinate requests waiting for token refresh
|
|
1237
1810
|
const refreshTokenSubject = new BehaviorSubject(null);
|
|
1238
1811
|
const authInterceptor = (req, next) => {
|
|
1239
1812
|
const router = inject(Router);
|
|
@@ -1273,24 +1846,19 @@ function handle401Error(request, next, http, router, config) {
|
|
|
1273
1846
|
if (refreshToken) {
|
|
1274
1847
|
return http.post(`${config.apiUrl}/auth/token/refresh/`, {
|
|
1275
1848
|
refresh: refreshToken
|
|
1276
|
-
}).pipe(switchMap((tokenResponse) => {
|
|
1849
|
+
}).pipe(switchMap$1((tokenResponse) => {
|
|
1277
1850
|
isRefreshing = false;
|
|
1278
|
-
// Update stored tokens
|
|
1279
1851
|
localStorage.setItem('ccvAccessToken', tokenResponse.access);
|
|
1280
1852
|
refreshTokenSubject.next(tokenResponse.access);
|
|
1281
|
-
// Notify AuthService that token was refreshed
|
|
1282
1853
|
if (typeof window !== 'undefined') {
|
|
1283
1854
|
window.dispatchEvent(new CustomEvent('tokenRefreshed'));
|
|
1284
1855
|
}
|
|
1285
|
-
// Retry the original request with new token
|
|
1286
1856
|
return next(addTokenToRequest(request, tokenResponse.access));
|
|
1287
1857
|
}), catchError((refreshError) => {
|
|
1288
|
-
// Refresh failed, clear tokens and redirect to login
|
|
1289
1858
|
isRefreshing = false;
|
|
1290
1859
|
localStorage.removeItem('ccvAccessToken');
|
|
1291
1860
|
localStorage.removeItem('ccvRefreshToken');
|
|
1292
1861
|
refreshTokenSubject.next(null);
|
|
1293
|
-
// Notify AuthService that auth was cleared
|
|
1294
1862
|
if (typeof window !== 'undefined') {
|
|
1295
1863
|
window.dispatchEvent(new CustomEvent('authCleared'));
|
|
1296
1864
|
}
|
|
@@ -1301,7 +1869,6 @@ function handle401Error(request, next, http, router, config) {
|
|
|
1301
1869
|
}));
|
|
1302
1870
|
}
|
|
1303
1871
|
else {
|
|
1304
|
-
// No refresh token, clear storage and redirect
|
|
1305
1872
|
isRefreshing = false;
|
|
1306
1873
|
localStorage.removeItem('ccvAccessToken');
|
|
1307
1874
|
localStorage.removeItem('ccvRefreshToken');
|
|
@@ -1316,8 +1883,7 @@ function handle401Error(request, next, http, router, config) {
|
|
|
1316
1883
|
}
|
|
1317
1884
|
}
|
|
1318
1885
|
else {
|
|
1319
|
-
|
|
1320
|
-
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
|
|
1886
|
+
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap$1((token) => {
|
|
1321
1887
|
return next(addTokenToRequest(request, token));
|
|
1322
1888
|
}));
|
|
1323
1889
|
}
|
|
@@ -1334,9 +1900,7 @@ class LoginComponent {
|
|
|
1334
1900
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1335
1901
|
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1336
1902
|
success = signal(null, ...(ngDevMode ? [{ debugName: "success" }] : []));
|
|
1337
|
-
// Observable for site configuration
|
|
1338
1903
|
siteConfig$ = this.siteConfigService.config$;
|
|
1339
|
-
// Auth configuration signals
|
|
1340
1904
|
authConfig = signal(null, ...(ngDevMode ? [{ debugName: "authConfig" }] : []));
|
|
1341
1905
|
registrationStatus = signal(null, ...(ngDevMode ? [{ debugName: "registrationStatus" }] : []));
|
|
1342
1906
|
constructor() {
|
|
@@ -1347,19 +1911,15 @@ class LoginComponent {
|
|
|
1347
1911
|
}
|
|
1348
1912
|
returnUrl = '/';
|
|
1349
1913
|
ngOnInit() {
|
|
1350
|
-
// Load auth configuration
|
|
1351
1914
|
this.loadAuthConfig();
|
|
1352
|
-
// Get return URL from query params
|
|
1353
1915
|
this.route.queryParams.subscribe(params => {
|
|
1354
|
-
this.returnUrl = params['returnUrl'] || '/';
|
|
1355
|
-
// Check for ORCID callback parameters
|
|
1916
|
+
this.returnUrl = this.cleanReturnUrl(params['returnUrl']) || '/';
|
|
1356
1917
|
if (params['code'] && params['state']) {
|
|
1357
1918
|
this.handleORCIDCallback(params['code'], params['state']);
|
|
1358
1919
|
}
|
|
1359
1920
|
else if (params['error']) {
|
|
1360
1921
|
this.error.set(`ORCID authentication failed: ${params['error']}`);
|
|
1361
1922
|
}
|
|
1362
|
-
// Check for registration success message
|
|
1363
1923
|
if (params['registered'] === 'true') {
|
|
1364
1924
|
this.success.set('Registration successful! You can now log in with your credentials.');
|
|
1365
1925
|
if (params['username']) {
|
|
@@ -1367,13 +1927,41 @@ class LoginComponent {
|
|
|
1367
1927
|
}
|
|
1368
1928
|
}
|
|
1369
1929
|
});
|
|
1370
|
-
// Subscribe to authentication state changes to handle token refresh scenarios
|
|
1371
1930
|
this.authService.isAuthenticated$.subscribe(isAuthenticated => {
|
|
1372
1931
|
if (isAuthenticated) {
|
|
1373
1932
|
this.router.navigate([this.returnUrl]);
|
|
1374
1933
|
}
|
|
1375
1934
|
});
|
|
1376
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Clean return URL to prevent accumulating login URLs
|
|
1938
|
+
*/
|
|
1939
|
+
cleanReturnUrl(returnUrl) {
|
|
1940
|
+
if (!returnUrl)
|
|
1941
|
+
return null;
|
|
1942
|
+
try {
|
|
1943
|
+
const url = new URL(returnUrl, window.location.origin);
|
|
1944
|
+
if (url.pathname === '/login') {
|
|
1945
|
+
const innerReturnUrl = url.searchParams.get('returnUrl');
|
|
1946
|
+
if (innerReturnUrl) {
|
|
1947
|
+
return this.cleanReturnUrl(innerReturnUrl);
|
|
1948
|
+
}
|
|
1949
|
+
return '/';
|
|
1950
|
+
}
|
|
1951
|
+
return url.pathname + url.search;
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
if (returnUrl.startsWith('/login')) {
|
|
1955
|
+
const urlParams = new URLSearchParams(returnUrl.split('?')[1]);
|
|
1956
|
+
const innerReturnUrl = urlParams.get('returnUrl');
|
|
1957
|
+
if (innerReturnUrl) {
|
|
1958
|
+
return this.cleanReturnUrl(innerReturnUrl);
|
|
1959
|
+
}
|
|
1960
|
+
return '/';
|
|
1961
|
+
}
|
|
1962
|
+
return returnUrl;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1377
1965
|
/**
|
|
1378
1966
|
* Load authentication configuration to determine available login options
|
|
1379
1967
|
*/
|
|
@@ -1384,7 +1972,6 @@ class LoginComponent {
|
|
|
1384
1972
|
},
|
|
1385
1973
|
error: (error) => {
|
|
1386
1974
|
console.warn('Could not load auth config:', error);
|
|
1387
|
-
// Use defaults if config loading fails
|
|
1388
1975
|
this.authConfig.set({
|
|
1389
1976
|
registrationEnabled: false,
|
|
1390
1977
|
orcidLoginEnabled: false,
|
|
@@ -1493,12 +2080,12 @@ class LoginComponent {
|
|
|
1493
2080
|
queryParams: { returnUrl: this.returnUrl }
|
|
1494
2081
|
});
|
|
1495
2082
|
}
|
|
1496
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1497
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.
|
|
2083
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2084
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: LoginComponent, isStandalone: true, selector: "ccc-login", ngImport: i0, template: "<div class=\"container\">\r\n <div class=\"login-wrapper\">\r\n <div class=\"card\">\r\n <div class=\"card-body\">\r\n @if (siteConfig$ | async; as config) {\r\n <h3 class=\"card-title text-center mb-4\">\r\n <i class=\"bi bi-flask me-2\"></i>{{ config.siteName }}\r\n </h3>\r\n }\r\n\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n {{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n {{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- ORCID Login Section -->\r\n @if (shouldShowOrcidLogin()) {\r\n <div class=\"mb-4\">\r\n <button \r\n type=\"button\" \r\n class=\"btn btn-primary w-100 mb-3\"\r\n [disabled]=\"loading()\"\r\n (click)=\"loginWithORCID()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\r\n }\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </button>\r\n <p class=\"text-muted text-center small\">\r\n Sign in with your ORCID account for seamless access\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- Divider -->\r\n @if (shouldShowOrcidLogin() && shouldShowRegularLogin()) {\r\n <div class=\"text-center mb-4\">\r\n <span class=\"text-muted\">or</span>\r\n </div>\r\n }\r\n\r\n <!-- Traditional Login Form -->\r\n @if (shouldShowRegularLogin()) {\r\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"mb-3\">\r\n <label for=\"username\" class=\"form-label\">Username</label>\r\n <div class=\"input-group\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-person\"></i>\r\n </span>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"username\"\r\n formControlName=\"username\"\r\n [class.is-invalid]=\"loginForm.get('username')?.invalid && loginForm.get('username')?.touched\"\r\n placeholder=\"Enter your username\">\r\n </div>\r\n @if (loginForm.get('username')?.invalid && loginForm.get('username')?.touched) {\r\n <div class=\"invalid-feedback d-block\">\r\n Username is required\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label for=\"password\" class=\"form-label\">Password</label>\r\n <div class=\"input-group\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-lock\"></i>\r\n </span>\r\n <input \r\n type=\"password\" \r\n class=\"form-control\" \r\n id=\"password\"\r\n formControlName=\"password\"\r\n [class.is-invalid]=\"loginForm.get('password')?.invalid && loginForm.get('password')?.touched\"\r\n placeholder=\"Enter your password\">\r\n </div>\r\n @if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {\r\n <div class=\"invalid-feedback d-block\">\r\n Password is required\r\n </div>\r\n }\r\n </div>\r\n\r\n <button \r\n type=\"submit\" \r\n class=\"btn btn-outline-primary w-100\"\r\n [disabled]=\"loginForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\r\n }\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Sign In\r\n </button>\r\n </form>\r\n }\r\n\r\n <!-- Registration Information -->\r\n @if (shouldShowRegistration()) {\r\n <div class=\"mt-4 text-center\">\r\n <div class=\"alert alert-info py-2\" role=\"alert\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n <strong>New Users:</strong> {{ registrationMessage() }}\r\n <div class=\"mt-2\">\r\n <button type=\"button\" class=\"btn btn-outline-primary btn-sm\" (click)=\"goToRegister()\">\r\n <i class=\"bi bi-person-plus me-1\"></i>Create Account\r\n </button>\r\n </div>\r\n <div class=\"mt-1\">\r\n <small class=\"text-muted\">\r\n @if (shouldShowOrcidLogin()) {\r\n <span>You can also sign in with ORCID to create an account</span>\r\n }\r\n </small>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Additional Information -->\r\n @if (siteConfig$ | async; as config) {\r\n <div class=\"mt-4 text-center\">\r\n <p class=\"text-muted small\">\r\n {{ config.siteName }} - Scientific Metadata Management\r\n </p>\r\n @if (shouldShowOrcidLogin() && !shouldShowRegistration()) {\r\n <p class=\"text-muted small\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n New users can sign in directly with ORCID\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;background:none;overflow:auto}.login-wrapper{width:100%;max-width:450px}.card{border:none;border-radius:12px;box-shadow:0 8px 24px var(--cupcake-shadow);background:var(--cupcake-card-bg);backdrop-filter:blur(10px);width:100%}.card-body{padding:2rem}.card-title{color:var(--cupcake-text);font-weight:600}.btn-primary{background:linear-gradient(135deg,var(--cupcake-primary) 0%,var(--cupcake-primary-dark) 100%);border:none;border-radius:8px;padding:12px;font-weight:500;transition:all .3s ease}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(var(--cupcake-primary-rgb),.3)}.btn-outline-primary{border-radius:8px;padding:12px;font-weight:500;transition:all .3s ease}.btn-outline-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(var(--cupcake-primary-rgb),.2)}.input-group-text{background:var(--cupcake-bg-tertiary);border-color:var(--cupcake-border);color:var(--cupcake-text-muted)}.form-control{border-radius:0 8px 8px 0;transition:all .3s ease}.form-control:focus{box-shadow:0 0 0 .2rem rgba(var(--cupcake-primary-rgb),.15);border-color:var(--cupcake-primary)}.input-group-text:first-child{border-radius:8px 0 0 8px}.spinner-border-sm{width:1rem;height:1rem}ngb-alert{border-radius:8px;border:none}ngb-alert.alert-success{background:var(--bs-success-bg-subtle, rgba(25, 135, 84, .1));color:var(--bs-success-text-emphasis, #0f5132);border:1px solid var(--bs-success-border-subtle, rgba(25, 135, 84, .2))}ngb-alert.alert-danger{background:var(--bs-danger-bg-subtle, rgba(220, 53, 69, .1));color:var(--bs-danger-text-emphasis, #842029);border:1px solid var(--bs-danger-border-subtle, rgba(220, 53, 69, .2))}:root[data-bs-theme=dark] ngb-alert.alert-success,.dark-mode ngb-alert.alert-success{background:#19875426;color:#75b798;border:1px solid rgba(25,135,84,.3)}:root[data-bs-theme=dark] ngb-alert.alert-danger,.dark-mode ngb-alert.alert-danger{background:#dc354526;color:#ea868f;border:1px solid rgba(220,53,69,.3)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
|
|
1498
2085
|
}
|
|
1499
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
2086
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LoginComponent, decorators: [{
|
|
1500
2087
|
type: Component,
|
|
1501
|
-
args: [{ selector: '
|
|
2088
|
+
args: [{ selector: 'ccc-login', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert], template: "<div class=\"container\">\r\n <div class=\"login-wrapper\">\r\n <div class=\"card\">\r\n <div class=\"card-body\">\r\n @if (siteConfig$ | async; as config) {\r\n <h3 class=\"card-title text-center mb-4\">\r\n <i class=\"bi bi-flask me-2\"></i>{{ config.siteName }}\r\n </h3>\r\n }\r\n\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n {{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n {{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- ORCID Login Section -->\r\n @if (shouldShowOrcidLogin()) {\r\n <div class=\"mb-4\">\r\n <button \r\n type=\"button\" \r\n class=\"btn btn-primary w-100 mb-3\"\r\n [disabled]=\"loading()\"\r\n (click)=\"loginWithORCID()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\r\n }\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </button>\r\n <p class=\"text-muted text-center small\">\r\n Sign in with your ORCID account for seamless access\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- Divider -->\r\n @if (shouldShowOrcidLogin() && shouldShowRegularLogin()) {\r\n <div class=\"text-center mb-4\">\r\n <span class=\"text-muted\">or</span>\r\n </div>\r\n }\r\n\r\n <!-- Traditional Login Form -->\r\n @if (shouldShowRegularLogin()) {\r\n <form [formGroup]=\"loginForm\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"mb-3\">\r\n <label for=\"username\" class=\"form-label\">Username</label>\r\n <div class=\"input-group\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-person\"></i>\r\n </span>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"username\"\r\n formControlName=\"username\"\r\n [class.is-invalid]=\"loginForm.get('username')?.invalid && loginForm.get('username')?.touched\"\r\n placeholder=\"Enter your username\">\r\n </div>\r\n @if (loginForm.get('username')?.invalid && loginForm.get('username')?.touched) {\r\n <div class=\"invalid-feedback d-block\">\r\n Username is required\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"mb-3\">\r\n <label for=\"password\" class=\"form-label\">Password</label>\r\n <div class=\"input-group\">\r\n <span class=\"input-group-text\">\r\n <i class=\"bi bi-lock\"></i>\r\n </span>\r\n <input \r\n type=\"password\" \r\n class=\"form-control\" \r\n id=\"password\"\r\n formControlName=\"password\"\r\n [class.is-invalid]=\"loginForm.get('password')?.invalid && loginForm.get('password')?.touched\"\r\n placeholder=\"Enter your password\">\r\n </div>\r\n @if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {\r\n <div class=\"invalid-feedback d-block\">\r\n Password is required\r\n </div>\r\n }\r\n </div>\r\n\r\n <button \r\n type=\"submit\" \r\n class=\"btn btn-outline-primary w-100\"\r\n [disabled]=\"loginForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\r\n }\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Sign In\r\n </button>\r\n </form>\r\n }\r\n\r\n <!-- Registration Information -->\r\n @if (shouldShowRegistration()) {\r\n <div class=\"mt-4 text-center\">\r\n <div class=\"alert alert-info py-2\" role=\"alert\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n <strong>New Users:</strong> {{ registrationMessage() }}\r\n <div class=\"mt-2\">\r\n <button type=\"button\" class=\"btn btn-outline-primary btn-sm\" (click)=\"goToRegister()\">\r\n <i class=\"bi bi-person-plus me-1\"></i>Create Account\r\n </button>\r\n </div>\r\n <div class=\"mt-1\">\r\n <small class=\"text-muted\">\r\n @if (shouldShowOrcidLogin()) {\r\n <span>You can also sign in with ORCID to create an account</span>\r\n }\r\n </small>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Additional Information -->\r\n @if (siteConfig$ | async; as config) {\r\n <div class=\"mt-4 text-center\">\r\n <p class=\"text-muted small\">\r\n {{ config.siteName }} - Scientific Metadata Management\r\n </p>\r\n @if (shouldShowOrcidLogin() && !shouldShowRegistration()) {\r\n <p class=\"text-muted small\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n New users can sign in directly with ORCID\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;background:none;overflow:auto}.login-wrapper{width:100%;max-width:450px}.card{border:none;border-radius:12px;box-shadow:0 8px 24px var(--cupcake-shadow);background:var(--cupcake-card-bg);backdrop-filter:blur(10px);width:100%}.card-body{padding:2rem}.card-title{color:var(--cupcake-text);font-weight:600}.btn-primary{background:linear-gradient(135deg,var(--cupcake-primary) 0%,var(--cupcake-primary-dark) 100%);border:none;border-radius:8px;padding:12px;font-weight:500;transition:all .3s ease}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(var(--cupcake-primary-rgb),.3)}.btn-outline-primary{border-radius:8px;padding:12px;font-weight:500;transition:all .3s ease}.btn-outline-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(var(--cupcake-primary-rgb),.2)}.input-group-text{background:var(--cupcake-bg-tertiary);border-color:var(--cupcake-border);color:var(--cupcake-text-muted)}.form-control{border-radius:0 8px 8px 0;transition:all .3s ease}.form-control:focus{box-shadow:0 0 0 .2rem rgba(var(--cupcake-primary-rgb),.15);border-color:var(--cupcake-primary)}.input-group-text:first-child{border-radius:8px 0 0 8px}.spinner-border-sm{width:1rem;height:1rem}ngb-alert{border-radius:8px;border:none}ngb-alert.alert-success{background:var(--bs-success-bg-subtle, rgba(25, 135, 84, .1));color:var(--bs-success-text-emphasis, #0f5132);border:1px solid var(--bs-success-border-subtle, rgba(25, 135, 84, .2))}ngb-alert.alert-danger{background:var(--bs-danger-bg-subtle, rgba(220, 53, 69, .1));color:var(--bs-danger-text-emphasis, #842029);border:1px solid var(--bs-danger-border-subtle, rgba(220, 53, 69, .2))}:root[data-bs-theme=dark] ngb-alert.alert-success,.dark-mode ngb-alert.alert-success{background:#19875426;color:#75b798;border:1px solid rgba(25,135,84,.3)}:root[data-bs-theme=dark] ngb-alert.alert-danger,.dark-mode ngb-alert.alert-danger{background:#dc354526;color:#ea868f;border:1px solid rgba(220,53,69,.3)}\n"] }]
|
|
1502
2089
|
}], ctorParameters: () => [] });
|
|
1503
2090
|
|
|
1504
2091
|
class RegisterComponent {
|
|
@@ -1511,9 +2098,7 @@ class RegisterComponent {
|
|
|
1511
2098
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1512
2099
|
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1513
2100
|
success = signal(null, ...(ngDevMode ? [{ debugName: "success" }] : []));
|
|
1514
|
-
// Observable for site configuration
|
|
1515
2101
|
siteConfig$ = this.siteConfigService.config$;
|
|
1516
|
-
// Registration status signals
|
|
1517
2102
|
registrationStatus = signal(null, ...(ngDevMode ? [{ debugName: "registrationStatus" }] : []));
|
|
1518
2103
|
registrationEnabled = signal(false, ...(ngDevMode ? [{ debugName: "registrationEnabled" }] : []));
|
|
1519
2104
|
returnUrl = '/login';
|
|
@@ -1528,9 +2113,7 @@ class RegisterComponent {
|
|
|
1528
2113
|
}, { validators: this.passwordMatchValidator });
|
|
1529
2114
|
}
|
|
1530
2115
|
ngOnInit() {
|
|
1531
|
-
// Check registration status
|
|
1532
2116
|
this.checkRegistrationStatus();
|
|
1533
|
-
// Get return URL from query params
|
|
1534
2117
|
this.route.queryParams.subscribe(params => {
|
|
1535
2118
|
this.returnUrl = params['returnUrl'] || '/login';
|
|
1536
2119
|
});
|
|
@@ -1582,7 +2165,6 @@ class RegisterComponent {
|
|
|
1582
2165
|
next: (response) => {
|
|
1583
2166
|
this.loading.set(false);
|
|
1584
2167
|
this.success.set(response.message || 'Registration successful! You can now log in with your credentials.');
|
|
1585
|
-
// Redirect to login after successful registration
|
|
1586
2168
|
setTimeout(() => {
|
|
1587
2169
|
this.router.navigate(['/login'], {
|
|
1588
2170
|
queryParams: {
|
|
@@ -1595,7 +2177,6 @@ class RegisterComponent {
|
|
|
1595
2177
|
},
|
|
1596
2178
|
error: (error) => {
|
|
1597
2179
|
this.loading.set(false);
|
|
1598
|
-
// Handle specific validation errors
|
|
1599
2180
|
if (error.error && typeof error.error === 'object') {
|
|
1600
2181
|
const errors = [];
|
|
1601
2182
|
for (const [field, messages] of Object.entries(error.error)) {
|
|
@@ -1693,12 +2274,12 @@ class RegisterComponent {
|
|
|
1693
2274
|
canSubmitForm = computed(() => {
|
|
1694
2275
|
return this.registrationForm.valid && this.registrationEnabled() && !this.loading();
|
|
1695
2276
|
}, ...(ngDevMode ? [{ debugName: "canSubmitForm" }] : []));
|
|
1696
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1697
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: RegisterComponent, isStandalone: true, selector: "app-register", ngImport: i0, template: "<div class=\"container\">\n <div class=\"register-wrapper\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (siteConfig$ | async; as config) {\n <h3 class=\"card-title text-center mb-4\">\n <i class=\"bi bi-person-plus me-2\"></i>Create Account - {{ config.siteName }}\n </h3>\n }\n\n <!-- Success Alert -->\n @if (success()) {\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\n </ngb-alert>\n }\n\n <!-- Error Alert -->\n @if (error()) {\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n <span [innerHTML]=\"error()?.replace('\\n', '<br>') || ''\"></span>\n </ngb-alert>\n }\n\n <!-- Registration Disabled Message -->\n @if (!registrationEnabled() && !loading()) {\n <div class=\"alert alert-warning text-center\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Registration Unavailable</strong>\n <div class=\"mt-2\">\n {{ registrationStatus()?.message || 'Registration is currently disabled' }}\n </div>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </div>\n }\n\n <!-- Registration Form -->\n @if (registrationEnabled()) {\n <form [formGroup]=\"registrationForm\" (ngSubmit)=\"onSubmit()\">\n <!-- Username -->\n <div class=\"mb-3\">\n <label for=\"username\" class=\"form-label\">Username <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-person\"></i>\n </span>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"username\"\n formControlName=\"username\"\n [class.is-invalid]=\"hasFieldError('username')\"\n placeholder=\"Choose a username\">\n </div>\n @if (getFieldErrorMessage('username')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('username') }}\n </div>\n }\n <div class=\"form-text\">\n Username must be 3-150 characters. Only letters, numbers, and @/./+/-/_ allowed.\n </div>\n </div>\n\n <!-- Email -->\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">Email Address <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-envelope\"></i>\n </span>\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"email\"\n formControlName=\"email\"\n [class.is-invalid]=\"hasFieldError('email')\"\n placeholder=\"Enter your email address\">\n </div>\n @if (getFieldErrorMessage('email')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('email') }}\n </div>\n }\n </div>\n\n <!-- First Name and Last Name -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"first_name\" class=\"form-label\">First Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"first_name\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"hasFieldError('firstName')\"\n placeholder=\"First name\">\n @if (getFieldErrorMessage('firstName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('firstName') }}\n </div>\n }\n </div>\n <div class=\"col-md-6 mb-3\">\n <label for=\"last_name\" class=\"form-label\">Last Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"last_name\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"hasFieldError('lastName')\"\n placeholder=\"Last name\">\n @if (getFieldErrorMessage('lastName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('lastName') }}\n </div>\n }\n </div>\n </div>\n\n <!-- Password -->\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"password\"\n formControlName=\"password\"\n [class.is-invalid]=\"hasFieldError('password')\"\n placeholder=\"Create a password\">\n </div>\n @if (getFieldErrorMessage('password')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('password') }}\n </div>\n }\n <div class=\"form-text\">\n Password must be at least 8 characters long.\n </div>\n </div>\n\n <!-- Confirm Password -->\n <div class=\"mb-4\">\n <label for=\"confirm_password\" class=\"form-label\">Confirm Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock-fill\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"confirm_password\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"hasFieldError('confirmPassword')\"\n placeholder=\"Confirm your password\">\n </div>\n @if (getFieldErrorMessage('confirmPassword')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('confirmPassword') }}\n </div>\n }\n </div>\n\n <!-- Submit Button -->\n <div class=\"d-grid gap-2 mb-3\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"registrationForm.invalid || loading() || !registrationEnabled()\">\n @if (loading()) {\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\n }\n @if (!loading()) {\n <i class=\"bi bi-person-plus me-2\"></i>\n }\n Create Account\n </button>\n </div>\n\n <!-- Back to Login -->\n <div class=\"text-center\">\n <p class=\"text-muted small mb-2\">Already have an account?</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </form>\n }\n\n <!-- Additional Information -->\n @if (siteConfig$ | async; as config) {\n <div class=\"mt-4 text-center\">\n <p class=\"text-muted small\">\n {{ config.siteName }} - Scientific Metadata Management\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}.register-wrapper{width:100%;max-width:500px}.card{border:none;border-radius:1rem;box-shadow:0 .5rem 1rem #00000026}.card-body{padding:3rem}.card-title{font-size:1.5rem;font-weight:600;color:var(--bs-primary)}.form-control{border-radius:.5rem;border:1px solid var(--bs-border-color);padding:.75rem 1rem}.form-control:focus{border-color:var(--bs-primary);box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.input-group-text{border-radius:.5rem 0 0 .5rem;background:var(--bs-light);border:1px solid var(--bs-border-color)}.input-group-text i{color:var(--bs-secondary)}.input-group .form-control{border-radius:0 .5rem .5rem 0}.btn{border-radius:.5rem;padding:.75rem 1.5rem;font-weight:500;transition:all .2s ease-in-out}.btn-primary{background:linear-gradient(135deg,var(--bs-primary) 0%,color-mix(in srgb,var(--bs-primary) 80%,black) 100%);border:none}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 .25rem .5rem rgba(var(--bs-primary-rgb),.3)}.btn-primary:disabled{transform:none;box-shadow:none}.alert{border-radius:.75rem;border:none}.alert.alert-success{background:rgba(var(--bs-success-rgb),.1);color:var(--bs-success);border-left:4px solid var(--bs-success)}.alert.alert-danger{background:rgba(var(--bs-danger-rgb),.1);color:var(--bs-danger);border-left:4px solid var(--bs-danger)}.alert.alert-warning{background:rgba(var(--bs-warning-rgb),.1);color:var(--bs-warning-emphasis);border-left:4px solid var(--bs-warning)}.invalid-feedback{font-size:.875rem;color:var(--bs-danger)}.form-text{font-size:.8rem;color:var(--bs-secondary)}@media (max-width: 576px){.card-body{padding:2rem 1.5rem}.card-title{font-size:1.25rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
|
|
2277
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RegisterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2278
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: RegisterComponent, isStandalone: true, selector: "ccc-register", ngImport: i0, template: "<div class=\"container\">\n <div class=\"register-wrapper\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (siteConfig$ | async; as config) {\n <h3 class=\"card-title text-center mb-4\">\n <i class=\"bi bi-person-plus me-2\"></i>Create Account - {{ config.siteName }}\n </h3>\n }\n\n <!-- Success Alert -->\n @if (success()) {\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\n </ngb-alert>\n }\n\n <!-- Error Alert -->\n @if (error()) {\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n <span [innerHTML]=\"error()?.replace('\\n', '<br>') || ''\"></span>\n </ngb-alert>\n }\n\n <!-- Registration Disabled Message -->\n @if (!registrationEnabled() && !loading()) {\n <div class=\"alert alert-warning text-center\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Registration Unavailable</strong>\n <div class=\"mt-2\">\n {{ registrationStatus()?.message || 'Registration is currently disabled' }}\n </div>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </div>\n }\n\n <!-- Registration Form -->\n @if (registrationEnabled()) {\n <form [formGroup]=\"registrationForm\" (ngSubmit)=\"onSubmit()\">\n <!-- Username -->\n <div class=\"mb-3\">\n <label for=\"username\" class=\"form-label\">Username <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-person\"></i>\n </span>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"username\"\n formControlName=\"username\"\n [class.is-invalid]=\"hasFieldError('username')\"\n placeholder=\"Choose a username\">\n </div>\n @if (getFieldErrorMessage('username')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('username') }}\n </div>\n }\n <div class=\"form-text\">\n Username must be 3-150 characters. Only letters, numbers, and @/./+/-/_ allowed.\n </div>\n </div>\n\n <!-- Email -->\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">Email Address <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-envelope\"></i>\n </span>\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"email\"\n formControlName=\"email\"\n [class.is-invalid]=\"hasFieldError('email')\"\n placeholder=\"Enter your email address\">\n </div>\n @if (getFieldErrorMessage('email')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('email') }}\n </div>\n }\n </div>\n\n <!-- First Name and Last Name -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"first_name\" class=\"form-label\">First Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"first_name\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"hasFieldError('firstName')\"\n placeholder=\"First name\">\n @if (getFieldErrorMessage('firstName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('firstName') }}\n </div>\n }\n </div>\n <div class=\"col-md-6 mb-3\">\n <label for=\"last_name\" class=\"form-label\">Last Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"last_name\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"hasFieldError('lastName')\"\n placeholder=\"Last name\">\n @if (getFieldErrorMessage('lastName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('lastName') }}\n </div>\n }\n </div>\n </div>\n\n <!-- Password -->\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"password\"\n formControlName=\"password\"\n [class.is-invalid]=\"hasFieldError('password')\"\n placeholder=\"Create a password\">\n </div>\n @if (getFieldErrorMessage('password')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('password') }}\n </div>\n }\n <div class=\"form-text\">\n Password must be at least 8 characters long.\n </div>\n </div>\n\n <!-- Confirm Password -->\n <div class=\"mb-4\">\n <label for=\"confirm_password\" class=\"form-label\">Confirm Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock-fill\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"confirm_password\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"hasFieldError('confirmPassword')\"\n placeholder=\"Confirm your password\">\n </div>\n @if (getFieldErrorMessage('confirmPassword')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('confirmPassword') }}\n </div>\n }\n </div>\n\n <!-- Submit Button -->\n <div class=\"d-grid gap-2 mb-3\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"registrationForm.invalid || loading() || !registrationEnabled()\">\n @if (loading()) {\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\n }\n @if (!loading()) {\n <i class=\"bi bi-person-plus me-2\"></i>\n }\n Create Account\n </button>\n </div>\n\n <!-- Back to Login -->\n <div class=\"text-center\">\n <p class=\"text-muted small mb-2\">Already have an account?</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </form>\n }\n\n <!-- Additional Information -->\n @if (siteConfig$ | async; as config) {\n <div class=\"mt-4 text-center\">\n <p class=\"text-muted small\">\n {{ config.siteName }} - Scientific Metadata Management\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}.register-wrapper{width:100%;max-width:500px}.card{border:none;border-radius:1rem;box-shadow:0 .5rem 1rem #00000026}.card-body{padding:3rem}.card-title{font-size:1.5rem;font-weight:600;color:var(--bs-primary)}.form-control{border-radius:.5rem;border:1px solid var(--bs-border-color);padding:.75rem 1rem}.form-control:focus{border-color:var(--bs-primary);box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.input-group-text{border-radius:.5rem 0 0 .5rem;background:var(--bs-light);border:1px solid var(--bs-border-color)}.input-group-text i{color:var(--bs-secondary)}.input-group .form-control{border-radius:0 .5rem .5rem 0}.btn{border-radius:.5rem;padding:.75rem 1.5rem;font-weight:500;transition:all .2s ease-in-out}.btn-primary{background:linear-gradient(135deg,var(--bs-primary) 0%,color-mix(in srgb,var(--bs-primary) 80%,black) 100%);border:none}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 .25rem .5rem rgba(var(--bs-primary-rgb),.3)}.btn-primary:disabled{transform:none;box-shadow:none}.alert{border-radius:.75rem;border:none}.alert.alert-success{background:rgba(var(--bs-success-rgb),.1);color:var(--bs-success);border-left:4px solid var(--bs-success)}.alert.alert-danger{background:rgba(var(--bs-danger-rgb),.1);color:var(--bs-danger);border-left:4px solid var(--bs-danger)}.alert.alert-warning{background:rgba(var(--bs-warning-rgb),.1);color:var(--bs-warning-emphasis);border-left:4px solid var(--bs-warning)}.invalid-feedback{font-size:.875rem;color:var(--bs-danger)}.form-text{font-size:.8rem;color:var(--bs-secondary)}@media (max-width: 576px){.card-body{padding:2rem 1.5rem}.card-title{font-size:1.25rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
|
|
1698
2279
|
}
|
|
1699
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
2280
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RegisterComponent, decorators: [{
|
|
1700
2281
|
type: Component,
|
|
1701
|
-
args: [{ selector: 'app-register', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert], template: "<div class=\"container\">\n <div class=\"register-wrapper\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (siteConfig$ | async; as config) {\n <h3 class=\"card-title text-center mb-4\">\n <i class=\"bi bi-person-plus me-2\"></i>Create Account - {{ config.siteName }}\n </h3>\n }\n\n <!-- Success Alert -->\n @if (success()) {\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\n </ngb-alert>\n }\n\n <!-- Error Alert -->\n @if (error()) {\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n <span [innerHTML]=\"error()?.replace('\\n', '<br>') || ''\"></span>\n </ngb-alert>\n }\n\n <!-- Registration Disabled Message -->\n @if (!registrationEnabled() && !loading()) {\n <div class=\"alert alert-warning text-center\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Registration Unavailable</strong>\n <div class=\"mt-2\">\n {{ registrationStatus()?.message || 'Registration is currently disabled' }}\n </div>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </div>\n }\n\n <!-- Registration Form -->\n @if (registrationEnabled()) {\n <form [formGroup]=\"registrationForm\" (ngSubmit)=\"onSubmit()\">\n <!-- Username -->\n <div class=\"mb-3\">\n <label for=\"username\" class=\"form-label\">Username <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-person\"></i>\n </span>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"username\"\n formControlName=\"username\"\n [class.is-invalid]=\"hasFieldError('username')\"\n placeholder=\"Choose a username\">\n </div>\n @if (getFieldErrorMessage('username')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('username') }}\n </div>\n }\n <div class=\"form-text\">\n Username must be 3-150 characters. Only letters, numbers, and @/./+/-/_ allowed.\n </div>\n </div>\n\n <!-- Email -->\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">Email Address <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-envelope\"></i>\n </span>\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"email\"\n formControlName=\"email\"\n [class.is-invalid]=\"hasFieldError('email')\"\n placeholder=\"Enter your email address\">\n </div>\n @if (getFieldErrorMessage('email')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('email') }}\n </div>\n }\n </div>\n\n <!-- First Name and Last Name -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"first_name\" class=\"form-label\">First Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"first_name\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"hasFieldError('firstName')\"\n placeholder=\"First name\">\n @if (getFieldErrorMessage('firstName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('firstName') }}\n </div>\n }\n </div>\n <div class=\"col-md-6 mb-3\">\n <label for=\"last_name\" class=\"form-label\">Last Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"last_name\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"hasFieldError('lastName')\"\n placeholder=\"Last name\">\n @if (getFieldErrorMessage('lastName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('lastName') }}\n </div>\n }\n </div>\n </div>\n\n <!-- Password -->\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"password\"\n formControlName=\"password\"\n [class.is-invalid]=\"hasFieldError('password')\"\n placeholder=\"Create a password\">\n </div>\n @if (getFieldErrorMessage('password')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('password') }}\n </div>\n }\n <div class=\"form-text\">\n Password must be at least 8 characters long.\n </div>\n </div>\n\n <!-- Confirm Password -->\n <div class=\"mb-4\">\n <label for=\"confirm_password\" class=\"form-label\">Confirm Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock-fill\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"confirm_password\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"hasFieldError('confirmPassword')\"\n placeholder=\"Confirm your password\">\n </div>\n @if (getFieldErrorMessage('confirmPassword')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('confirmPassword') }}\n </div>\n }\n </div>\n\n <!-- Submit Button -->\n <div class=\"d-grid gap-2 mb-3\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"registrationForm.invalid || loading() || !registrationEnabled()\">\n @if (loading()) {\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\n }\n @if (!loading()) {\n <i class=\"bi bi-person-plus me-2\"></i>\n }\n Create Account\n </button>\n </div>\n\n <!-- Back to Login -->\n <div class=\"text-center\">\n <p class=\"text-muted small mb-2\">Already have an account?</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </form>\n }\n\n <!-- Additional Information -->\n @if (siteConfig$ | async; as config) {\n <div class=\"mt-4 text-center\">\n <p class=\"text-muted small\">\n {{ config.siteName }} - Scientific Metadata Management\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}.register-wrapper{width:100%;max-width:500px}.card{border:none;border-radius:1rem;box-shadow:0 .5rem 1rem #00000026}.card-body{padding:3rem}.card-title{font-size:1.5rem;font-weight:600;color:var(--bs-primary)}.form-control{border-radius:.5rem;border:1px solid var(--bs-border-color);padding:.75rem 1rem}.form-control:focus{border-color:var(--bs-primary);box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.input-group-text{border-radius:.5rem 0 0 .5rem;background:var(--bs-light);border:1px solid var(--bs-border-color)}.input-group-text i{color:var(--bs-secondary)}.input-group .form-control{border-radius:0 .5rem .5rem 0}.btn{border-radius:.5rem;padding:.75rem 1.5rem;font-weight:500;transition:all .2s ease-in-out}.btn-primary{background:linear-gradient(135deg,var(--bs-primary) 0%,color-mix(in srgb,var(--bs-primary) 80%,black) 100%);border:none}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 .25rem .5rem rgba(var(--bs-primary-rgb),.3)}.btn-primary:disabled{transform:none;box-shadow:none}.alert{border-radius:.75rem;border:none}.alert.alert-success{background:rgba(var(--bs-success-rgb),.1);color:var(--bs-success);border-left:4px solid var(--bs-success)}.alert.alert-danger{background:rgba(var(--bs-danger-rgb),.1);color:var(--bs-danger);border-left:4px solid var(--bs-danger)}.alert.alert-warning{background:rgba(var(--bs-warning-rgb),.1);color:var(--bs-warning-emphasis);border-left:4px solid var(--bs-warning)}.invalid-feedback{font-size:.875rem;color:var(--bs-danger)}.form-text{font-size:.8rem;color:var(--bs-secondary)}@media (max-width: 576px){.card-body{padding:2rem 1.5rem}.card-title{font-size:1.25rem}}\n"] }]
|
|
2282
|
+
args: [{ selector: 'ccc-register', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert], template: "<div class=\"container\">\n <div class=\"register-wrapper\">\n <div class=\"card\">\n <div class=\"card-body\">\n @if (siteConfig$ | async; as config) {\n <h3 class=\"card-title text-center mb-4\">\n <i class=\"bi bi-person-plus me-2\"></i>Create Account - {{ config.siteName }}\n </h3>\n }\n\n <!-- Success Alert -->\n @if (success()) {\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\n </ngb-alert>\n }\n\n <!-- Error Alert -->\n @if (error()) {\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n <span [innerHTML]=\"error()?.replace('\\n', '<br>') || ''\"></span>\n </ngb-alert>\n }\n\n <!-- Registration Disabled Message -->\n @if (!registrationEnabled() && !loading()) {\n <div class=\"alert alert-warning text-center\">\n <i class=\"bi bi-info-circle me-2\"></i>\n <strong>Registration Unavailable</strong>\n <div class=\"mt-2\">\n {{ registrationStatus()?.message || 'Registration is currently disabled' }}\n </div>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-outline-primary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </div>\n }\n\n <!-- Registration Form -->\n @if (registrationEnabled()) {\n <form [formGroup]=\"registrationForm\" (ngSubmit)=\"onSubmit()\">\n <!-- Username -->\n <div class=\"mb-3\">\n <label for=\"username\" class=\"form-label\">Username <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-person\"></i>\n </span>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"username\"\n formControlName=\"username\"\n [class.is-invalid]=\"hasFieldError('username')\"\n placeholder=\"Choose a username\">\n </div>\n @if (getFieldErrorMessage('username')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('username') }}\n </div>\n }\n <div class=\"form-text\">\n Username must be 3-150 characters. Only letters, numbers, and @/./+/-/_ allowed.\n </div>\n </div>\n\n <!-- Email -->\n <div class=\"mb-3\">\n <label for=\"email\" class=\"form-label\">Email Address <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-envelope\"></i>\n </span>\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"email\"\n formControlName=\"email\"\n [class.is-invalid]=\"hasFieldError('email')\"\n placeholder=\"Enter your email address\">\n </div>\n @if (getFieldErrorMessage('email')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('email') }}\n </div>\n }\n </div>\n\n <!-- First Name and Last Name -->\n <div class=\"row\">\n <div class=\"col-md-6 mb-3\">\n <label for=\"first_name\" class=\"form-label\">First Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"first_name\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"hasFieldError('firstName')\"\n placeholder=\"First name\">\n @if (getFieldErrorMessage('firstName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('firstName') }}\n </div>\n }\n </div>\n <div class=\"col-md-6 mb-3\">\n <label for=\"last_name\" class=\"form-label\">Last Name <span class=\"text-danger\">*</span></label>\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"last_name\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"hasFieldError('lastName')\"\n placeholder=\"Last name\">\n @if (getFieldErrorMessage('lastName')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('lastName') }}\n </div>\n }\n </div>\n </div>\n\n <!-- Password -->\n <div class=\"mb-3\">\n <label for=\"password\" class=\"form-label\">Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"password\"\n formControlName=\"password\"\n [class.is-invalid]=\"hasFieldError('password')\"\n placeholder=\"Create a password\">\n </div>\n @if (getFieldErrorMessage('password')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('password') }}\n </div>\n }\n <div class=\"form-text\">\n Password must be at least 8 characters long.\n </div>\n </div>\n\n <!-- Confirm Password -->\n <div class=\"mb-4\">\n <label for=\"confirm_password\" class=\"form-label\">Confirm Password <span class=\"text-danger\">*</span></label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-lock-fill\"></i>\n </span>\n <input \n type=\"password\" \n class=\"form-control\" \n id=\"confirm_password\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"hasFieldError('confirmPassword')\"\n placeholder=\"Confirm your password\">\n </div>\n @if (getFieldErrorMessage('confirmPassword')) {\n <div class=\"invalid-feedback d-block\">\n {{ getFieldErrorMessage('confirmPassword') }}\n </div>\n }\n </div>\n\n <!-- Submit Button -->\n <div class=\"d-grid gap-2 mb-3\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"registrationForm.invalid || loading() || !registrationEnabled()\">\n @if (loading()) {\n <span class=\"spinner-border spinner-border-sm me-2\" aria-hidden=\"true\"></span>\n }\n @if (!loading()) {\n <i class=\"bi bi-person-plus me-2\"></i>\n }\n Create Account\n </button>\n </div>\n\n <!-- Back to Login -->\n <div class=\"text-center\">\n <p class=\"text-muted small mb-2\">Already have an account?</p>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"goToLogin()\">\n <i class=\"bi bi-arrow-left me-1\"></i>Back to Login\n </button>\n </div>\n </form>\n }\n\n <!-- Additional Information -->\n @if (siteConfig$ | async; as config) {\n <div class=\"mt-4 text-center\">\n <p class=\"text-muted small\">\n {{ config.siteName }} - Scientific Metadata Management\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n", styles: [".container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1rem}.register-wrapper{width:100%;max-width:500px}.card{border:none;border-radius:1rem;box-shadow:0 .5rem 1rem #00000026}.card-body{padding:3rem}.card-title{font-size:1.5rem;font-weight:600;color:var(--bs-primary)}.form-control{border-radius:.5rem;border:1px solid var(--bs-border-color);padding:.75rem 1rem}.form-control:focus{border-color:var(--bs-primary);box-shadow:0 0 0 .2rem rgba(var(--bs-primary-rgb),.25)}.input-group-text{border-radius:.5rem 0 0 .5rem;background:var(--bs-light);border:1px solid var(--bs-border-color)}.input-group-text i{color:var(--bs-secondary)}.input-group .form-control{border-radius:0 .5rem .5rem 0}.btn{border-radius:.5rem;padding:.75rem 1.5rem;font-weight:500;transition:all .2s ease-in-out}.btn-primary{background:linear-gradient(135deg,var(--bs-primary) 0%,color-mix(in srgb,var(--bs-primary) 80%,black) 100%);border:none}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 .25rem .5rem rgba(var(--bs-primary-rgb),.3)}.btn-primary:disabled{transform:none;box-shadow:none}.alert{border-radius:.75rem;border:none}.alert.alert-success{background:rgba(var(--bs-success-rgb),.1);color:var(--bs-success);border-left:4px solid var(--bs-success)}.alert.alert-danger{background:rgba(var(--bs-danger-rgb),.1);color:var(--bs-danger);border-left:4px solid var(--bs-danger)}.alert.alert-warning{background:rgba(var(--bs-warning-rgb),.1);color:var(--bs-warning-emphasis);border-left:4px solid var(--bs-warning)}.invalid-feedback{font-size:.875rem;color:var(--bs-danger)}.form-text{font-size:.8rem;color:var(--bs-secondary)}@media (max-width: 576px){.card-body{padding:2rem 1.5rem}.card-title{font-size:1.25rem}}\n"] }]
|
|
1702
2283
|
}], ctorParameters: () => [] });
|
|
1703
2284
|
|
|
1704
2285
|
class UserManagementComponent {
|
|
@@ -1912,12 +2493,12 @@ class UserManagementComponent {
|
|
|
1912
2493
|
getUserDisplayName(user) {
|
|
1913
2494
|
return this.userManagementService.getUserDisplayName(user);
|
|
1914
2495
|
}
|
|
1915
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1916
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: UserManagementComponent, isStandalone: true, selector: "app-user-management", ngImport: i0, template: "<div class=\"user-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-people-fill me-2 text-primary\"></i>User Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ totalUsers() }} users</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateUserModal(createUserModal)\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"users-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by username, email, or name\">\n </div>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isStaff\" class=\"form-label\">Staff Status</label>\n <select class=\"form-select\" id=\"isStaff\" formControlName=\"isStaff\">\n <option value=\"\">All Users</option>\n <option value=\"true\">Staff Only</option>\n <option value=\"false\">Regular Users</option>\n </select>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isActive\" class=\"form-label\">Account Status</label>\n <select class=\"form-select\" id=\"isActive\" formControlName=\"isActive\">\n <option value=\"\">All Accounts</option>\n <option value=\"true\">Active Only</option>\n <option value=\"false\">Inactive Only</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Success/Error Messages -->\n @if (successMessage()) {\n <div class=\"alert alert-success alert-dismissible\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ successMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n @if (errorMessage()) {\n <div class=\"alert alert-danger alert-dismissible\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n <!-- Users Table -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Users\n <span class=\"badge bg-primary ms-2\">{{ users().length }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasResults()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th>User</th>\n <th>Contact</th>\n <th>Status</th>\n <th>Joined</th>\n <th>Last Login</th>\n <th class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (user of users(); track user.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center\">\n <div>\n <div class=\"fw-semibold\">{{ getUserDisplayName(user) }}</div>\n <small class=\"text-muted\">@{{ user.username }}</small>\n </div>\n </div>\n </td>\n <td>\n <div>{{ user.email }}</div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (user.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n @if (user.isStaff) {\n <span class=\"badge bg-warning\">Staff</span>\n }\n </div>\n </td>\n <td>{{ formatDate(user.dateJoined) }}</td>\n <td>{{ formatDate(user.lastLogin) }}</td>\n <td class=\"text-end\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditUserModal(editUserModal, user)\"\n title=\"Edit user\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning\"\n (click)=\"openPasswordResetModal(passwordResetModal, user)\"\n title=\"Reset password\">\n <i class=\"bi bi-shield-lock\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isActive ? 'btn-outline-secondary' : 'btn-outline-success'\"\n (click)=\"toggleUserStatus(user)\"\n [title]=\"user.isActive ? 'Deactivate user' : 'Activate user'\">\n <i class=\"bi\" [class]=\"user.isActive ? 'bi-person-dash' : 'bi-person-check'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isStaff ? 'btn-outline-secondary' : 'btn-outline-info'\"\n (click)=\"toggleStaffStatus(user)\"\n [title]=\"user.isStaff ? 'Remove staff privileges' : 'Grant staff privileges'\">\n <i class=\"bi\" [class]=\"user.isStaff ? 'bi-person-x' : 'bi-person-badge'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteUser(user)\"\n title=\"Delete user\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"text-muted\">\n Showing {{ showingFrom() }} to {{ showingTo() }} of {{ totalUsers() }} users\n </div>\n <nav>\n <ul class=\"pagination pagination-sm mb-0\">\n <li class=\"page-item\" [class.disabled]=\"!canGoToPreviousPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() - 1)\" [disabled]=\"!canGoToPreviousPage()\">\n <i class=\"bi bi-chevron-left\"></i>\n </button>\n </li>\n @for (page of pages(); track page) {\n <li class=\"page-item\" [class.active]=\"page === currentPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(page)\">{{ page }}</button>\n </li>\n }\n <li class=\"page-item\" [class.disabled]=\"!canGoToNextPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() + 1)\" [disabled]=\"!canGoToNextPage()\">\n <i class=\"bi bi-chevron-right\"></i>\n </button>\n </li>\n </ul>\n </nav>\n </div>\n }\n } @else {\n <div class=\"text-center p-4\">\n <i class=\"bi bi-people display-4 text-muted\"></i>\n <p class=\"text-muted mt-2\">No users found</p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Create User Modal -->\n<ng-template #createUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-person-plus me-2\"></i>Create User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n <form #createForm=\"ngForm\" (ngSubmit)=\"createUser(createForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"createUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createUsername\"\n name=\"username\"\n ngModel\n required\n #usernameField=\"ngModel\">\n @if (usernameField.invalid && usernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"createEmail\"\n name=\"email\"\n ngModel\n required\n email\n #emailField=\"ngModel\">\n @if (emailField.invalid && emailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createFirstName\"\n name=\"firstName\"\n ngModel\n required\n #firstNameField=\"ngModel\">\n @if (firstNameField.invalid && firstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createLastName\"\n name=\"lastName\"\n ngModel\n required\n #lastNameField=\"ngModel\">\n @if (lastNameField.invalid && lastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createPassword\" class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPassword\"\n name=\"password\"\n ngModel\n required\n minlength=\"8\"\n #passwordField=\"ngModel\">\n @if (passwordField.invalid && passwordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createPasswordConfirm\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPasswordConfirm\"\n name=\"password_confirm\"\n ngModel\n required\n #passwordConfirmField=\"ngModel\">\n @if (passwordConfirmField.invalid && passwordConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsStaff\"\n name=\"isStaff\"\n ngModel>\n <label class=\"form-check-label\" for=\"createIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsActive\"\n name=\"isActive\"\n ngModel\n checked>\n <label class=\"form-check-label\" for=\"createIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"createUser(createForm.value)\"\n [disabled]=\"createForm.invalid || !canCreateUser()\">\n @if (isCreatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n</ng-template>\n\n<!-- Edit User Modal -->\n<ng-template #editUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil me-2\"></i>Edit User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <form #editForm=\"ngForm\" id=\"editUserForm\" (ngSubmit)=\"updateUser(selectedUser()!.id!, editForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"editUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editUsername\"\n name=\"username\"\n [ngModel]=\"selectedUser()!.username\"\n required\n #editUsernameField=\"ngModel\">\n @if (editUsernameField.invalid && editUsernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"editEmail\"\n name=\"email\"\n [ngModel]=\"selectedUser()!.email\"\n required\n email\n #editEmailField=\"ngModel\">\n @if (editEmailField.invalid && editEmailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"editFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editFirstName\"\n name=\"firstName\"\n [ngModel]=\"selectedUser()!.firstName\"\n required\n #editFirstNameField=\"ngModel\">\n @if (editFirstNameField.invalid && editFirstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editLastName\"\n name=\"lastName\"\n [ngModel]=\"selectedUser()!.lastName\"\n required\n #editLastNameField=\"ngModel\">\n @if (editLastNameField.invalid && editLastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsStaff\"\n name=\"isStaff\"\n [ngModel]=\"selectedUser()!.isStaff\">\n <label class=\"form-check-label\" for=\"editIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsActive\"\n name=\"isActive\"\n [ngModel]=\"selectedUser()!.isActive\">\n <label class=\"form-check-label\" for=\"editIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n form=\"editUserForm\"\n [disabled]=\"!canUpdateUser()\">\n @if (isUpdatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>Update User\n </button>\n }\n </div>\n</ng-template>\n\n<!-- Password Reset Modal -->\n<ng-template #passwordResetModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-shield-lock me-2\"></i>Reset Password\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n You are about to reset the password for user <strong>{{ selectedUserDisplayName() }}</strong>.\n </div>\n \n <form #resetForm=\"ngForm\" id=\"resetPasswordForm\" (ngSubmit)=\"resetUserPassword(selectedUser()!.id!, resetForm.value)\">\n <div class=\"mb-3\">\n <label for=\"resetNewPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetNewPassword\"\n name=\"new_password\"\n ngModel\n required\n minlength=\"8\"\n #resetPasswordField=\"ngModel\">\n @if (resetPasswordField.invalid && resetPasswordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"resetConfirmPassword\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetConfirmPassword\"\n name=\"confirm_password\"\n ngModel\n required\n #resetConfirmField=\"ngModel\">\n @if (resetConfirmField.invalid && resetConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"forcePasswordChange\"\n name=\"force_password_change\"\n ngModel>\n <label class=\"form-check-label\" for=\"forcePasswordChange\">\n Force user to change password on next login\n </label>\n </div>\n </div>\n <div class=\"mb-3\">\n <label for=\"resetReason\" class=\"form-label\">Reason (optional)</label>\n <textarea\n class=\"form-control\"\n id=\"resetReason\"\n name=\"reason\"\n ngModel\n rows=\"2\"\n placeholder=\"Optional reason for password reset\"></textarea>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-warning\"\n form=\"resetPasswordForm\"\n [disabled]=\"!canResetPassword()\">\n @if (isResettingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-lock me-1\"></i>Reset Password\n </button>\n }\n </div>\n</ng-template>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i1$1.EmailValidator, selector: "[email][formControlName],[email][formControl],[email][ngModel]", inputs: ["email"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: NgbModule }] });
|
|
2496
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2497
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: UserManagementComponent, isStandalone: true, selector: "ccc-user-management", ngImport: i0, template: "<div class=\"user-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-people-fill me-2 text-primary\"></i>User Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ totalUsers() }} users</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateUserModal(createUserModal)\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"users-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by username, email, or name\">\n </div>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isStaff\" class=\"form-label\">Staff Status</label>\n <select class=\"form-select\" id=\"isStaff\" formControlName=\"isStaff\">\n <option value=\"\">All Users</option>\n <option value=\"true\">Staff Only</option>\n <option value=\"false\">Regular Users</option>\n </select>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isActive\" class=\"form-label\">Account Status</label>\n <select class=\"form-select\" id=\"isActive\" formControlName=\"isActive\">\n <option value=\"\">All Accounts</option>\n <option value=\"true\">Active Only</option>\n <option value=\"false\">Inactive Only</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Success/Error Messages -->\n @if (successMessage()) {\n <div class=\"alert alert-success alert-dismissible\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ successMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n @if (errorMessage()) {\n <div class=\"alert alert-danger alert-dismissible\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n <!-- Users Table -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Users\n <span class=\"badge bg-primary ms-2\">{{ users().length }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasResults()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th>User</th>\n <th>Contact</th>\n <th>Status</th>\n <th>Joined</th>\n <th>Last Login</th>\n <th class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (user of users(); track user.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center\">\n <div>\n <div class=\"fw-semibold\">{{ getUserDisplayName(user) }}</div>\n <small class=\"text-muted\">@{{ user.username }}</small>\n </div>\n </div>\n </td>\n <td>\n <div>{{ user.email }}</div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (user.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n @if (user.isStaff) {\n <span class=\"badge bg-warning\">Staff</span>\n }\n </div>\n </td>\n <td>{{ formatDate(user.dateJoined) }}</td>\n <td>{{ formatDate(user.lastLogin) }}</td>\n <td class=\"text-end\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditUserModal(editUserModal, user)\"\n title=\"Edit user\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning\"\n (click)=\"openPasswordResetModal(passwordResetModal, user)\"\n title=\"Reset password\">\n <i class=\"bi bi-shield-lock\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isActive ? 'btn-outline-secondary' : 'btn-outline-success'\"\n (click)=\"toggleUserStatus(user)\"\n [title]=\"user.isActive ? 'Deactivate user' : 'Activate user'\">\n <i class=\"bi\" [class]=\"user.isActive ? 'bi-person-dash' : 'bi-person-check'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isStaff ? 'btn-outline-secondary' : 'btn-outline-info'\"\n (click)=\"toggleStaffStatus(user)\"\n [title]=\"user.isStaff ? 'Remove staff privileges' : 'Grant staff privileges'\">\n <i class=\"bi\" [class]=\"user.isStaff ? 'bi-person-x' : 'bi-person-badge'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteUser(user)\"\n title=\"Delete user\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"text-muted\">\n Showing {{ showingFrom() }} to {{ showingTo() }} of {{ totalUsers() }} users\n </div>\n <nav>\n <ul class=\"pagination pagination-sm mb-0\">\n <li class=\"page-item\" [class.disabled]=\"!canGoToPreviousPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() - 1)\" [disabled]=\"!canGoToPreviousPage()\">\n <i class=\"bi bi-chevron-left\"></i>\n </button>\n </li>\n @for (page of pages(); track page) {\n <li class=\"page-item\" [class.active]=\"page === currentPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(page)\">{{ page }}</button>\n </li>\n }\n <li class=\"page-item\" [class.disabled]=\"!canGoToNextPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() + 1)\" [disabled]=\"!canGoToNextPage()\">\n <i class=\"bi bi-chevron-right\"></i>\n </button>\n </li>\n </ul>\n </nav>\n </div>\n }\n } @else {\n <div class=\"text-center p-4\">\n <i class=\"bi bi-people display-4 text-muted\"></i>\n <p class=\"text-muted mt-2\">No users found</p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Create User Modal -->\n<ng-template #createUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-person-plus me-2\"></i>Create User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n <form #createForm=\"ngForm\" (ngSubmit)=\"createUser(createForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"createUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createUsername\"\n name=\"username\"\n ngModel\n required\n #usernameField=\"ngModel\">\n @if (usernameField.invalid && usernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"createEmail\"\n name=\"email\"\n ngModel\n required\n email\n #emailField=\"ngModel\">\n @if (emailField.invalid && emailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createFirstName\"\n name=\"firstName\"\n ngModel\n required\n #firstNameField=\"ngModel\">\n @if (firstNameField.invalid && firstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createLastName\"\n name=\"lastName\"\n ngModel\n required\n #lastNameField=\"ngModel\">\n @if (lastNameField.invalid && lastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createPassword\" class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPassword\"\n name=\"password\"\n ngModel\n required\n minlength=\"8\"\n #passwordField=\"ngModel\">\n @if (passwordField.invalid && passwordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createPasswordConfirm\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPasswordConfirm\"\n name=\"password_confirm\"\n ngModel\n required\n #passwordConfirmField=\"ngModel\">\n @if (passwordConfirmField.invalid && passwordConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsStaff\"\n name=\"isStaff\"\n ngModel>\n <label class=\"form-check-label\" for=\"createIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsActive\"\n name=\"isActive\"\n ngModel\n checked>\n <label class=\"form-check-label\" for=\"createIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"createUser(createForm.value)\"\n [disabled]=\"createForm.invalid || !canCreateUser()\">\n @if (isCreatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n</ng-template>\n\n<!-- Edit User Modal -->\n<ng-template #editUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil me-2\"></i>Edit User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <form #editForm=\"ngForm\" id=\"editUserForm\" (ngSubmit)=\"updateUser(selectedUser()!.id!, editForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"editUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editUsername\"\n name=\"username\"\n [ngModel]=\"selectedUser()!.username\"\n required\n #editUsernameField=\"ngModel\">\n @if (editUsernameField.invalid && editUsernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"editEmail\"\n name=\"email\"\n [ngModel]=\"selectedUser()!.email\"\n required\n email\n #editEmailField=\"ngModel\">\n @if (editEmailField.invalid && editEmailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"editFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editFirstName\"\n name=\"firstName\"\n [ngModel]=\"selectedUser()!.firstName\"\n required\n #editFirstNameField=\"ngModel\">\n @if (editFirstNameField.invalid && editFirstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editLastName\"\n name=\"lastName\"\n [ngModel]=\"selectedUser()!.lastName\"\n required\n #editLastNameField=\"ngModel\">\n @if (editLastNameField.invalid && editLastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsStaff\"\n name=\"isStaff\"\n [ngModel]=\"selectedUser()!.isStaff\">\n <label class=\"form-check-label\" for=\"editIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsActive\"\n name=\"isActive\"\n [ngModel]=\"selectedUser()!.isActive\">\n <label class=\"form-check-label\" for=\"editIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n form=\"editUserForm\"\n [disabled]=\"!canUpdateUser()\">\n @if (isUpdatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>Update User\n </button>\n }\n </div>\n</ng-template>\n\n<!-- Password Reset Modal -->\n<ng-template #passwordResetModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-shield-lock me-2\"></i>Reset Password\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n You are about to reset the password for user <strong>{{ selectedUserDisplayName() }}</strong>.\n </div>\n \n <form #resetForm=\"ngForm\" id=\"resetPasswordForm\" (ngSubmit)=\"resetUserPassword(selectedUser()!.id!, resetForm.value)\">\n <div class=\"mb-3\">\n <label for=\"resetNewPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetNewPassword\"\n name=\"new_password\"\n ngModel\n required\n minlength=\"8\"\n #resetPasswordField=\"ngModel\">\n @if (resetPasswordField.invalid && resetPasswordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"resetConfirmPassword\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetConfirmPassword\"\n name=\"confirm_password\"\n ngModel\n required\n #resetConfirmField=\"ngModel\">\n @if (resetConfirmField.invalid && resetConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"forcePasswordChange\"\n name=\"force_password_change\"\n ngModel>\n <label class=\"form-check-label\" for=\"forcePasswordChange\">\n Force user to change password on next login\n </label>\n </div>\n </div>\n <div class=\"mb-3\">\n <label for=\"resetReason\" class=\"form-label\">Reason (optional)</label>\n <textarea\n class=\"form-control\"\n id=\"resetReason\"\n name=\"reason\"\n ngModel\n rows=\"2\"\n placeholder=\"Optional reason for password reset\"></textarea>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-warning\"\n form=\"resetPasswordForm\"\n [disabled]=\"!canResetPassword()\">\n @if (isResettingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-lock me-1\"></i>Reset Password\n </button>\n }\n </div>\n</ng-template>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i1$1.EmailValidator, selector: "[email][formControlName],[email][formControl],[email][ngModel]", inputs: ["email"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: NgbModule }] });
|
|
1917
2498
|
}
|
|
1918
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
2499
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserManagementComponent, decorators: [{
|
|
1919
2500
|
type: Component,
|
|
1920
|
-
args: [{ selector: 'app-user-management', standalone: true, imports: [CommonModule, ReactiveFormsModule, FormsModule, NgbModule], template: "<div class=\"user-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-people-fill me-2 text-primary\"></i>User Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ totalUsers() }} users</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateUserModal(createUserModal)\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"users-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by username, email, or name\">\n </div>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isStaff\" class=\"form-label\">Staff Status</label>\n <select class=\"form-select\" id=\"isStaff\" formControlName=\"isStaff\">\n <option value=\"\">All Users</option>\n <option value=\"true\">Staff Only</option>\n <option value=\"false\">Regular Users</option>\n </select>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isActive\" class=\"form-label\">Account Status</label>\n <select class=\"form-select\" id=\"isActive\" formControlName=\"isActive\">\n <option value=\"\">All Accounts</option>\n <option value=\"true\">Active Only</option>\n <option value=\"false\">Inactive Only</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Success/Error Messages -->\n @if (successMessage()) {\n <div class=\"alert alert-success alert-dismissible\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ successMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n @if (errorMessage()) {\n <div class=\"alert alert-danger alert-dismissible\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n <!-- Users Table -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Users\n <span class=\"badge bg-primary ms-2\">{{ users().length }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasResults()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th>User</th>\n <th>Contact</th>\n <th>Status</th>\n <th>Joined</th>\n <th>Last Login</th>\n <th class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (user of users(); track user.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center\">\n <div>\n <div class=\"fw-semibold\">{{ getUserDisplayName(user) }}</div>\n <small class=\"text-muted\">@{{ user.username }}</small>\n </div>\n </div>\n </td>\n <td>\n <div>{{ user.email }}</div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (user.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n @if (user.isStaff) {\n <span class=\"badge bg-warning\">Staff</span>\n }\n </div>\n </td>\n <td>{{ formatDate(user.dateJoined) }}</td>\n <td>{{ formatDate(user.lastLogin) }}</td>\n <td class=\"text-end\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditUserModal(editUserModal, user)\"\n title=\"Edit user\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning\"\n (click)=\"openPasswordResetModal(passwordResetModal, user)\"\n title=\"Reset password\">\n <i class=\"bi bi-shield-lock\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isActive ? 'btn-outline-secondary' : 'btn-outline-success'\"\n (click)=\"toggleUserStatus(user)\"\n [title]=\"user.isActive ? 'Deactivate user' : 'Activate user'\">\n <i class=\"bi\" [class]=\"user.isActive ? 'bi-person-dash' : 'bi-person-check'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isStaff ? 'btn-outline-secondary' : 'btn-outline-info'\"\n (click)=\"toggleStaffStatus(user)\"\n [title]=\"user.isStaff ? 'Remove staff privileges' : 'Grant staff privileges'\">\n <i class=\"bi\" [class]=\"user.isStaff ? 'bi-person-x' : 'bi-person-badge'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteUser(user)\"\n title=\"Delete user\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"text-muted\">\n Showing {{ showingFrom() }} to {{ showingTo() }} of {{ totalUsers() }} users\n </div>\n <nav>\n <ul class=\"pagination pagination-sm mb-0\">\n <li class=\"page-item\" [class.disabled]=\"!canGoToPreviousPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() - 1)\" [disabled]=\"!canGoToPreviousPage()\">\n <i class=\"bi bi-chevron-left\"></i>\n </button>\n </li>\n @for (page of pages(); track page) {\n <li class=\"page-item\" [class.active]=\"page === currentPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(page)\">{{ page }}</button>\n </li>\n }\n <li class=\"page-item\" [class.disabled]=\"!canGoToNextPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() + 1)\" [disabled]=\"!canGoToNextPage()\">\n <i class=\"bi bi-chevron-right\"></i>\n </button>\n </li>\n </ul>\n </nav>\n </div>\n }\n } @else {\n <div class=\"text-center p-4\">\n <i class=\"bi bi-people display-4 text-muted\"></i>\n <p class=\"text-muted mt-2\">No users found</p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Create User Modal -->\n<ng-template #createUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-person-plus me-2\"></i>Create User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n <form #createForm=\"ngForm\" (ngSubmit)=\"createUser(createForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"createUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createUsername\"\n name=\"username\"\n ngModel\n required\n #usernameField=\"ngModel\">\n @if (usernameField.invalid && usernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"createEmail\"\n name=\"email\"\n ngModel\n required\n email\n #emailField=\"ngModel\">\n @if (emailField.invalid && emailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createFirstName\"\n name=\"firstName\"\n ngModel\n required\n #firstNameField=\"ngModel\">\n @if (firstNameField.invalid && firstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createLastName\"\n name=\"lastName\"\n ngModel\n required\n #lastNameField=\"ngModel\">\n @if (lastNameField.invalid && lastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createPassword\" class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPassword\"\n name=\"password\"\n ngModel\n required\n minlength=\"8\"\n #passwordField=\"ngModel\">\n @if (passwordField.invalid && passwordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createPasswordConfirm\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPasswordConfirm\"\n name=\"password_confirm\"\n ngModel\n required\n #passwordConfirmField=\"ngModel\">\n @if (passwordConfirmField.invalid && passwordConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsStaff\"\n name=\"isStaff\"\n ngModel>\n <label class=\"form-check-label\" for=\"createIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsActive\"\n name=\"isActive\"\n ngModel\n checked>\n <label class=\"form-check-label\" for=\"createIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"createUser(createForm.value)\"\n [disabled]=\"createForm.invalid || !canCreateUser()\">\n @if (isCreatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n</ng-template>\n\n<!-- Edit User Modal -->\n<ng-template #editUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil me-2\"></i>Edit User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <form #editForm=\"ngForm\" id=\"editUserForm\" (ngSubmit)=\"updateUser(selectedUser()!.id!, editForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"editUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editUsername\"\n name=\"username\"\n [ngModel]=\"selectedUser()!.username\"\n required\n #editUsernameField=\"ngModel\">\n @if (editUsernameField.invalid && editUsernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"editEmail\"\n name=\"email\"\n [ngModel]=\"selectedUser()!.email\"\n required\n email\n #editEmailField=\"ngModel\">\n @if (editEmailField.invalid && editEmailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"editFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editFirstName\"\n name=\"firstName\"\n [ngModel]=\"selectedUser()!.firstName\"\n required\n #editFirstNameField=\"ngModel\">\n @if (editFirstNameField.invalid && editFirstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editLastName\"\n name=\"lastName\"\n [ngModel]=\"selectedUser()!.lastName\"\n required\n #editLastNameField=\"ngModel\">\n @if (editLastNameField.invalid && editLastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsStaff\"\n name=\"isStaff\"\n [ngModel]=\"selectedUser()!.isStaff\">\n <label class=\"form-check-label\" for=\"editIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsActive\"\n name=\"isActive\"\n [ngModel]=\"selectedUser()!.isActive\">\n <label class=\"form-check-label\" for=\"editIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n form=\"editUserForm\"\n [disabled]=\"!canUpdateUser()\">\n @if (isUpdatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>Update User\n </button>\n }\n </div>\n</ng-template>\n\n<!-- Password Reset Modal -->\n<ng-template #passwordResetModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-shield-lock me-2\"></i>Reset Password\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n You are about to reset the password for user <strong>{{ selectedUserDisplayName() }}</strong>.\n </div>\n \n <form #resetForm=\"ngForm\" id=\"resetPasswordForm\" (ngSubmit)=\"resetUserPassword(selectedUser()!.id!, resetForm.value)\">\n <div class=\"mb-3\">\n <label for=\"resetNewPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetNewPassword\"\n name=\"new_password\"\n ngModel\n required\n minlength=\"8\"\n #resetPasswordField=\"ngModel\">\n @if (resetPasswordField.invalid && resetPasswordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"resetConfirmPassword\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetConfirmPassword\"\n name=\"confirm_password\"\n ngModel\n required\n #resetConfirmField=\"ngModel\">\n @if (resetConfirmField.invalid && resetConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"forcePasswordChange\"\n name=\"force_password_change\"\n ngModel>\n <label class=\"form-check-label\" for=\"forcePasswordChange\">\n Force user to change password on next login\n </label>\n </div>\n </div>\n <div class=\"mb-3\">\n <label for=\"resetReason\" class=\"form-label\">Reason (optional)</label>\n <textarea\n class=\"form-control\"\n id=\"resetReason\"\n name=\"reason\"\n ngModel\n rows=\"2\"\n placeholder=\"Optional reason for password reset\"></textarea>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-warning\"\n form=\"resetPasswordForm\"\n [disabled]=\"!canResetPassword()\">\n @if (isResettingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-lock me-1\"></i>Reset Password\n </button>\n }\n </div>\n</ng-template>" }]
|
|
2501
|
+
args: [{ selector: 'ccc-user-management', standalone: true, imports: [CommonModule, ReactiveFormsModule, FormsModule, NgbModule], template: "<div class=\"user-management-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-people-fill me-2 text-primary\"></i>User Management\n </h4>\n <span class=\"badge bg-primary ms-3\">{{ totalUsers() }} users</span>\n </div>\n\n <div class=\"d-flex gap-2\">\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n (click)=\"openCreateUserModal(createUserModal)\"\n [disabled]=\"isLoading()\">\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"users-content p-4\">\n <!-- Search and Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-4\">\n <label for=\"search\" class=\"form-label\">Search</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\">\n <i class=\"bi bi-search\"></i>\n </span>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"search\"\n formControlName=\"search\"\n placeholder=\"Search by username, email, or name\">\n </div>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isStaff\" class=\"form-label\">Staff Status</label>\n <select class=\"form-select\" id=\"isStaff\" formControlName=\"isStaff\">\n <option value=\"\">All Users</option>\n <option value=\"true\">Staff Only</option>\n <option value=\"false\">Regular Users</option>\n </select>\n </div>\n\n <div class=\"col-md-4\">\n <label for=\"isActive\" class=\"form-label\">Account Status</label>\n <select class=\"form-select\" id=\"isActive\" formControlName=\"isActive\">\n <option value=\"\">All Accounts</option>\n <option value=\"true\">Active Only</option>\n <option value=\"false\">Inactive Only</option>\n </select>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Success/Error Messages -->\n @if (successMessage()) {\n <div class=\"alert alert-success alert-dismissible\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ successMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n @if (errorMessage()) {\n <div class=\"alert alert-danger alert-dismissible\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n <button type=\"button\" class=\"btn-close\" (click)=\"clearMessages()\"></button>\n </div>\n }\n\n <!-- Users Table -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-table me-2\"></i>Users\n <span class=\"badge bg-primary ms-2\">{{ users().length }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasResults()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th>User</th>\n <th>Contact</th>\n <th>Status</th>\n <th>Joined</th>\n <th>Last Login</th>\n <th class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (user of users(); track user.id) {\n <tr>\n <td>\n <div class=\"d-flex align-items-center\">\n <div>\n <div class=\"fw-semibold\">{{ getUserDisplayName(user) }}</div>\n <small class=\"text-muted\">@{{ user.username }}</small>\n </div>\n </div>\n </td>\n <td>\n <div>{{ user.email }}</div>\n </td>\n <td>\n <div class=\"d-flex gap-1 flex-wrap\">\n @if (user.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n @if (user.isStaff) {\n <span class=\"badge bg-warning\">Staff</span>\n }\n </div>\n </td>\n <td>{{ formatDate(user.dateJoined) }}</td>\n <td>{{ formatDate(user.lastLogin) }}</td>\n <td class=\"text-end\">\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button\n type=\"button\"\n class=\"btn btn-outline-primary\"\n (click)=\"openEditUserModal(editUserModal, user)\"\n title=\"Edit user\">\n <i class=\"bi bi-pencil\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-warning\"\n (click)=\"openPasswordResetModal(passwordResetModal, user)\"\n title=\"Reset password\">\n <i class=\"bi bi-shield-lock\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isActive ? 'btn-outline-secondary' : 'btn-outline-success'\"\n (click)=\"toggleUserStatus(user)\"\n [title]=\"user.isActive ? 'Deactivate user' : 'Activate user'\">\n <i class=\"bi\" [class]=\"user.isActive ? 'bi-person-dash' : 'bi-person-check'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn\"\n [class]=\"user.isStaff ? 'btn-outline-secondary' : 'btn-outline-info'\"\n (click)=\"toggleStaffStatus(user)\"\n [title]=\"user.isStaff ? 'Remove staff privileges' : 'Grant staff privileges'\">\n <i class=\"bi\" [class]=\"user.isStaff ? 'bi-person-x' : 'bi-person-badge'\"></i>\n </button>\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"deleteUser(user)\"\n title=\"Delete user\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (totalPages() > 1) {\n <div class=\"d-flex justify-content-between align-items-center p-3 border-top\">\n <div class=\"text-muted\">\n Showing {{ showingFrom() }} to {{ showingTo() }} of {{ totalUsers() }} users\n </div>\n <nav>\n <ul class=\"pagination pagination-sm mb-0\">\n <li class=\"page-item\" [class.disabled]=\"!canGoToPreviousPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() - 1)\" [disabled]=\"!canGoToPreviousPage()\">\n <i class=\"bi bi-chevron-left\"></i>\n </button>\n </li>\n @for (page of pages(); track page) {\n <li class=\"page-item\" [class.active]=\"page === currentPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(page)\">{{ page }}</button>\n </li>\n }\n <li class=\"page-item\" [class.disabled]=\"!canGoToNextPage()\">\n <button class=\"page-link\" (click)=\"onPageChange(currentPage() + 1)\" [disabled]=\"!canGoToNextPage()\">\n <i class=\"bi bi-chevron-right\"></i>\n </button>\n </li>\n </ul>\n </nav>\n </div>\n }\n } @else {\n <div class=\"text-center p-4\">\n <i class=\"bi bi-people display-4 text-muted\"></i>\n <p class=\"text-muted mt-2\">No users found</p>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Create User Modal -->\n<ng-template #createUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-person-plus me-2\"></i>Create User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n <form #createForm=\"ngForm\" (ngSubmit)=\"createUser(createForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"createUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createUsername\"\n name=\"username\"\n ngModel\n required\n #usernameField=\"ngModel\">\n @if (usernameField.invalid && usernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"createEmail\"\n name=\"email\"\n ngModel\n required\n email\n #emailField=\"ngModel\">\n @if (emailField.invalid && emailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createFirstName\"\n name=\"firstName\"\n ngModel\n required\n #firstNameField=\"ngModel\">\n @if (firstNameField.invalid && firstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"createLastName\"\n name=\"lastName\"\n ngModel\n required\n #lastNameField=\"ngModel\">\n @if (lastNameField.invalid && lastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"createPassword\" class=\"form-label\">Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPassword\"\n name=\"password\"\n ngModel\n required\n minlength=\"8\"\n #passwordField=\"ngModel\">\n @if (passwordField.invalid && passwordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"createPasswordConfirm\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"createPasswordConfirm\"\n name=\"password_confirm\"\n ngModel\n required\n #passwordConfirmField=\"ngModel\">\n @if (passwordConfirmField.invalid && passwordConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsStaff\"\n name=\"isStaff\"\n ngModel>\n <label class=\"form-check-label\" for=\"createIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"createIsActive\"\n name=\"isActive\"\n ngModel\n checked>\n <label class=\"form-check-label\" for=\"createIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n <button\n type=\"button\"\n class=\"btn btn-primary\"\n (click)=\"createUser(createForm.value)\"\n [disabled]=\"createForm.invalid || !canCreateUser()\">\n @if (isCreatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-person-plus me-1\"></i>Create User\n </button>\n </div>\n</ng-template>\n\n<!-- Edit User Modal -->\n<ng-template #editUserModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-pencil me-2\"></i>Edit User\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <form #editForm=\"ngForm\" id=\"editUserForm\" (ngSubmit)=\"updateUser(selectedUser()!.id!, editForm.value)\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"editUsername\" class=\"form-label\">Username</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editUsername\"\n name=\"username\"\n [ngModel]=\"selectedUser()!.username\"\n required\n #editUsernameField=\"ngModel\">\n @if (editUsernameField.invalid && editUsernameField.touched) {\n <div class=\"text-danger small\">Username is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editEmail\" class=\"form-label\">Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"editEmail\"\n name=\"email\"\n [ngModel]=\"selectedUser()!.email\"\n required\n email\n #editEmailField=\"ngModel\">\n @if (editEmailField.invalid && editEmailField.touched) {\n <div class=\"text-danger small\">Valid email is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <label for=\"editFirstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editFirstName\"\n name=\"firstName\"\n [ngModel]=\"selectedUser()!.firstName\"\n required\n #editFirstNameField=\"ngModel\">\n @if (editFirstNameField.invalid && editFirstNameField.touched) {\n <div class=\"text-danger small\">First name is required</div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"editLastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"editLastName\"\n name=\"lastName\"\n [ngModel]=\"selectedUser()!.lastName\"\n required\n #editLastNameField=\"ngModel\">\n @if (editLastNameField.invalid && editLastNameField.touched) {\n <div class=\"text-danger small\">Last name is required</div>\n }\n </div>\n </div>\n <div class=\"row g-3 mt-1\">\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsStaff\"\n name=\"isStaff\"\n [ngModel]=\"selectedUser()!.isStaff\">\n <label class=\"form-check-label\" for=\"editIsStaff\">\n Staff privileges\n </label>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"editIsActive\"\n name=\"isActive\"\n [ngModel]=\"selectedUser()!.isActive\">\n <label class=\"form-check-label\" for=\"editIsActive\">\n Active account\n </label>\n </div>\n </div>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n form=\"editUserForm\"\n [disabled]=\"!canUpdateUser()\">\n @if (isUpdatingUser()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>Update User\n </button>\n }\n </div>\n</ng-template>\n\n<!-- Password Reset Modal -->\n<ng-template #passwordResetModal let-modal>\n <div class=\"modal-header\">\n <h4 class=\"modal-title\">\n <i class=\"bi bi-shield-lock me-2\"></i>Reset Password\n </h4>\n <button type=\"button\" class=\"btn-close\" (click)=\"modal.dismiss()\"></button>\n </div>\n <div class=\"modal-body\">\n @if (hasSelectedUser()) {\n <div class=\"alert alert-warning\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\n You are about to reset the password for user <strong>{{ selectedUserDisplayName() }}</strong>.\n </div>\n \n <form #resetForm=\"ngForm\" id=\"resetPasswordForm\" (ngSubmit)=\"resetUserPassword(selectedUser()!.id!, resetForm.value)\">\n <div class=\"mb-3\">\n <label for=\"resetNewPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetNewPassword\"\n name=\"new_password\"\n ngModel\n required\n minlength=\"8\"\n #resetPasswordField=\"ngModel\">\n @if (resetPasswordField.invalid && resetPasswordField.touched) {\n <div class=\"text-danger small\">Password must be at least 8 characters</div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"resetConfirmPassword\" class=\"form-label\">Confirm Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"resetConfirmPassword\"\n name=\"confirm_password\"\n ngModel\n required\n #resetConfirmField=\"ngModel\">\n @if (resetConfirmField.invalid && resetConfirmField.touched) {\n <div class=\"text-danger small\">Password confirmation is required</div>\n }\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"forcePasswordChange\"\n name=\"force_password_change\"\n ngModel>\n <label class=\"form-check-label\" for=\"forcePasswordChange\">\n Force user to change password on next login\n </label>\n </div>\n </div>\n <div class=\"mb-3\">\n <label for=\"resetReason\" class=\"form-label\">Reason (optional)</label>\n <textarea\n class=\"form-control\"\n id=\"resetReason\"\n name=\"reason\"\n ngModel\n rows=\"2\"\n placeholder=\"Optional reason for password reset\"></textarea>\n </div>\n </form>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"modal.dismiss()\">\n <i class=\"bi bi-x-circle me-1\"></i>Cancel\n </button>\n @if (hasSelectedUser()) {\n <button\n type=\"submit\"\n class=\"btn btn-warning\"\n form=\"resetPasswordForm\"\n [disabled]=\"!canResetPassword()\">\n @if (isResettingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-lock me-1\"></i>Reset Password\n </button>\n }\n </div>\n</ng-template>" }]
|
|
1921
2502
|
}], ctorParameters: () => [] });
|
|
1922
2503
|
|
|
1923
2504
|
class LabGroupsComponent {
|
|
@@ -2202,12 +2783,12 @@ class LabGroupsComponent {
|
|
|
2202
2783
|
this.pendingInvitations.set([]);
|
|
2203
2784
|
this.showInviteForm.set(false);
|
|
2204
2785
|
}
|
|
2205
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
2206
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: LabGroupsComponent, isStandalone: true, selector: "app-lab-groups", ngImport: i0, template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
|
|
2786
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LabGroupsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2787
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: LabGroupsComponent, isStandalone: true, selector: "ccc-lab-groups", ngImport: i0, template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2$1.NgbPagination, selector: "ngb-pagination", inputs: ["disabled", "boundaryLinks", "directionLinks", "ellipses", "rotate", "collectionSize", "maxSize", "page", "pageSize", "size"], outputs: ["pageChange"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] });
|
|
2207
2788
|
}
|
|
2208
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
2789
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LabGroupsComponent, decorators: [{
|
|
2209
2790
|
type: Component,
|
|
2210
|
-
args: [{ selector: 'app-lab-groups', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"] }]
|
|
2791
|
+
args: [{ selector: 'ccc-lab-groups', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"lab-groups-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"navbar-brand m-0 fw-bold\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n </h4>\n </div>\n <div class=\"d-flex gap-2\">\n <button type=\"button\" class=\"btn btn-primary btn-sm\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>New Lab Group\n </button>\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"groups-content p-4\">\n <!-- Create Group Form -->\n @if (showCreateForm()) {\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-plus-circle me-2\"></i>Create New Lab Group\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"createGroupForm\" (ngSubmit)=\"createLabGroup()\" class=\"row g-3\">\n <div class=\"col-md-6\">\n <div class=\"form-floating\">\n <input \n type=\"text\" \n class=\"form-control\" \n id=\"groupName\" \n formControlName=\"name\"\n [class.is-invalid]=\"createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched\"\n placeholder=\"Group name\">\n <label for=\"groupName\">Group Name *</label>\n </div>\n @if (createGroupForm.get('name')?.invalid && createGroupForm.get('name')?.touched) {\n <div class=\"invalid-feedback\">\n @if (createGroupForm.get('name')?.errors?.['required']) {\n Group name is required\n }\n @if (createGroupForm.get('name')?.errors?.['minlength']) {\n Group name must be at least 2 characters\n }\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <div class=\"form-check form-switch mt-3\">\n <input \n class=\"form-check-input\" \n type=\"checkbox\" \n id=\"allowInvites\" \n formControlName=\"allowMemberInvites\">\n <label class=\"form-check-label\" for=\"allowInvites\">\n <i class=\"bi bi-envelope me-1\"></i>Allow members to invite others\n </label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"groupDescription\" \n formControlName=\"description\"\n style=\"height: 100px\"\n placeholder=\"Description\"></textarea>\n <label for=\"groupDescription\">Description (optional)</label>\n </div>\n </div>\n <div class=\"col-12\">\n <div class=\"d-flex gap-2\">\n <button \n type=\"submit\" \n class=\"btn btn-primary\"\n [disabled]=\"createGroupForm.invalid || isCreatingGroup()\">\n @if (isCreatingGroup()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Create Group\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"toggleCreateForm()\">\n Cancel\n </button>\n </div>\n </div>\n </form>\n </div>\n </div>\n }\n\n <!-- Search & Filters Section -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-search me-2\"></i>Search & Filters\n </h5>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"searchForm\" class=\"row g-3\">\n <div class=\"col-md-12\">\n <div class=\"form-floating\">\n <input \n type=\"search\" \n class=\"form-control\" \n id=\"groupSearch\" \n formControlName=\"search\"\n placeholder=\"Search groups...\">\n <label for=\"groupSearch\">\n <i class=\"bi bi-search me-1\"></i>Search Groups\n </label>\n </div>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Groups List -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Lab Groups\n <span class=\"badge bg-primary ms-2\">{{ labGroupsData().count }}</span>\n </h5>\n @if (isLoading()) {\n <div class=\"spinner-border spinner-border-sm text-primary\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n }\n </div>\n <div class=\"card-body p-0\">\n @if (hasLabGroups()) {\n <div class=\"table-responsive\">\n <table class=\"table table-hover mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\"><i class=\"bi bi-tag me-1\"></i>Group Name</th>\n <th scope=\"col\"><i class=\"bi bi-person me-1\"></i>Creator</th>\n <th scope=\"col\"><i class=\"bi bi-people me-1\"></i>Members</th>\n <th scope=\"col\"><i class=\"bi bi-check-circle me-1\"></i>Status</th>\n <th scope=\"col\"><i class=\"bi bi-calendar me-1\"></i>Created</th>\n <th scope=\"col\" style=\"width: 120px;\"><i class=\"bi bi-gear me-1\"></i>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (group of labGroupsData().results; track group.id) {\n <tr>\n <td>\n <strong>{{ group.name }}</strong>\n @if (group.description) {\n <br><small class=\"text-muted\">{{ group.description }}</small>\n }\n </td>\n <td>{{ group.creatorName || 'Unknown' }}</td>\n <td>\n <span class=\"badge bg-info\">{{ group.memberCount }}</span>\n </td>\n <td>\n @if (group.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-secondary\">Inactive</span>\n }\n </td>\n <td>\n <small>{{ group.createdAt | date:'short' }}</small>\n </td>\n <td>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button \n type=\"button\"\n class=\"btn btn-outline-info\" \n (click)=\"viewGroupMembers(group)\"\n title=\"View members\">\n <i class=\"bi bi-people\"></i>\n </button>\n @if (group.isMember && !group.isCreator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-warning\" \n (click)=\"leaveGroup(group)\"\n title=\"Leave group\">\n <i class=\"bi bi-box-arrow-right\"></i>\n </button>\n }\n @if (group.canManage) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger\" \n (click)=\"deleteGroup(group)\"\n title=\"Delete group\">\n <i class=\"bi bi-trash\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n @if (showPagination()) {\n <div class=\"d-flex justify-content-center mt-3 p-3\">\n <ngb-pagination\n [collectionSize]=\"labGroupsData().count\"\n [page]=\"currentPage()\"\n [pageSize]=\"pageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onPageChange($event)\"\n class=\"pagination-sm\">\n </ngb-pagination>\n </div>\n }\n } @else {\n <div class=\"text-center py-5\">\n <div class=\"text-muted\">\n <i class=\"bi bi-people display-1 mb-3\"></i>\n <h5>No Lab Groups Found</h5>\n <p>\n @if (hasSearchValue()) {\n Try adjusting your search criteria or create a new lab group.\n } @else {\n Get started by creating your first lab group.\n }\n </p>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"toggleCreateForm()\">\n <i class=\"bi bi-plus me-1\"></i>Create First Lab Group\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n</div>\n\n<!-- Group Members Modal -->\n@if (selectedGroupForMembers()) {\n <div class=\"modal-backdrop fade show\"></div>\n <div class=\"modal fade show d-block\" tabindex=\"-1\">\n <div class=\"modal-dialog modal-lg\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <h5 class=\"modal-title\">\n <i class=\"bi bi-people me-2\"></i>{{ currentGroupName() }} - Members\n </h5>\n <button type=\"button\" class=\"btn-close\" (click)=\"closeGroupDetails()\"></button>\n </div>\n <div class=\"modal-body\">\n <!-- Invite Form -->\n @if (canInviteToCurrentGroup()) {\n <div class=\"card mb-4\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Invite Member\n </h6>\n <button \n type=\"button\"\n class=\"btn btn-sm btn-outline-primary\" \n (click)=\"toggleInviteForm()\">\n @if (showInviteForm()) {\n Cancel\n } @else {\n <i class=\"bi bi-plus me-1\"></i>Invite\n }\n </button>\n </div>\n @if (showInviteForm()) {\n <div class=\"card-body\">\n <form [formGroup]=\"inviteForm\" (ngSubmit)=\"inviteMember()\" class=\"row g-3\">\n <div class=\"col-md-8\">\n <div class=\"form-floating\">\n <input \n type=\"email\" \n class=\"form-control\" \n id=\"inviteEmail\" \n formControlName=\"invitedEmail\"\n [class.is-invalid]=\"inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched\"\n placeholder=\"Email address\">\n <label for=\"inviteEmail\">Email Address *</label>\n </div>\n @if (inviteForm.get('invitedEmail')?.invalid && inviteForm.get('invitedEmail')?.touched) {\n <div class=\"invalid-feedback\">\n @if (inviteForm.get('invitedEmail')?.errors?.['required']) {\n Email is required\n }\n @if (inviteForm.get('invitedEmail')?.errors?.['email']) {\n Please enter a valid email address\n }\n </div>\n }\n </div>\n <div class=\"col-md-4\">\n <button \n type=\"submit\" \n class=\"btn btn-primary w-100 h-100\"\n [disabled]=\"inviteForm.invalid || isInviting()\">\n @if (isInviting()) {\n <span class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n }\n Send Invite\n </button>\n </div>\n <div class=\"col-12\">\n <div class=\"form-floating\">\n <textarea \n class=\"form-control\" \n id=\"inviteMessage\" \n formControlName=\"message\"\n style=\"height: 80px\"\n placeholder=\"Message\"></textarea>\n <label for=\"inviteMessage\">Message (optional)</label>\n </div>\n </div>\n </form>\n </div>\n }\n </div>\n }\n\n <!-- Current Members -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n @if (hasGroupMembers()) {\n <div class=\"list-group list-group-flush\">\n @for (member of groupMembers(); track member.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ member.firstName }} {{ member.lastName }}</strong>\n <br>\n <small class=\"text-muted\">{{ member.email }}</small>\n @if (selectedGroupForMembers() && member.id === selectedGroupForMembers()!.creator) {\n <span class=\"badge bg-warning ms-2\">Creator</span>\n }\n </div>\n @if (canManageCurrentGroup() && selectedGroupForMembers() && member.id !== selectedGroupForMembers()!.creator) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"removeMember(member.id)\"\n title=\"Remove member\">\n <i class=\"bi bi-person-dash\"></i>\n </button>\n }\n </div>\n }\n </div>\n } @else {\n <div class=\"text-center py-3 text-muted\">\n <i class=\"bi bi-people fs-4 mb-2\"></i>\n <div>No members found</div>\n </div>\n }\n </div>\n </div>\n\n <!-- Pending Invitations -->\n @if (hasPendingInvitations()) {\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-clock me-2\"></i>Pending Invitations ({{ pendingInvitationsCount() }})\n </h6>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of pendingInvitations(); track invitation.id) {\n <div class=\"list-group-item d-flex justify-content-between align-items-center\">\n <div>\n <strong>{{ invitation.invitedEmail }}</strong>\n <br>\n <small class=\"text-muted\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'short' }}\n </small>\n @if (invitation.message) {\n <br>\n <small class=\"text-info\">{{ invitation.message }}</small>\n }\n </div>\n @if (canManageCurrentGroup()) {\n <button \n type=\"button\"\n class=\"btn btn-outline-danger btn-sm\" \n (click)=\"cancelInvitation(invitation.id)\"\n title=\"Cancel invitation\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n </div>\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"closeGroupDetails()\">\n Close\n </button>\n </div>\n </div>\n </div>\n </div>\n}", styles: [".lab-groups-container .groups-content{max-width:1200px;margin:0 auto}@media (max-width: 768px){.lab-groups-container .groups-content{padding:1rem!important}.lab-groups-container .btn-group{flex-direction:column;width:100%}.lab-groups-container .btn-group .btn{border-radius:.25rem!important;margin-bottom:.25rem}.lab-groups-container .modal-dialog{margin:.5rem}}\n"] }]
|
|
2211
2792
|
}], ctorParameters: () => [] });
|
|
2212
2793
|
|
|
2213
2794
|
class UserProfileComponent {
|
|
@@ -2366,12 +2947,12 @@ class UserProfileComponent {
|
|
|
2366
2947
|
}
|
|
2367
2948
|
return null;
|
|
2368
2949
|
}
|
|
2369
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
2370
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: UserProfileComponent, isStandalone: true, selector: "app-user-profile", ngImport: i0, template: "<div class=\"user-profile-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-person-circle me-2 text-primary\"></i>User Profile\n </h4>\n @if (isStaff()) {\n <span class=\"badge bg-warning ms-3\">Staff</span>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"profile-content p-4\">\n @if (isLoading()) {\n <div class=\"text-center\">\n <div class=\"spinner-border text-primary me-2\"></div>\n <span>Loading profile...</span>\n </div>\n } @else {\n <!-- User Info Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Account Information\n </h5>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Full Name:</small>\n <div class=\"fw-semibold\">{{ fullName() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Username:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.username }}</div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Email:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.email }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Account Status:</small>\n <div>\n @if (currentUser()?.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n </div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Member Since:</small>\n <div class=\"fw-semibold\">{{ joinDate() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Last Login:</small>\n <div class=\"fw-semibold\">{{ lastLogin() }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Navigation Tabs -->\n <ul ngbNav #profileNav=\"ngbNav\" [activeId]=\"activeTab()\" (activeIdChange)=\"setActiveTab($event)\" class=\"nav-tabs\">\n <li [ngbNavItem]=\"'profile'\">\n <button ngbNavLink (click)=\"setActiveTab('profile')\">\n <i class=\"bi bi-person me-1\"></i>Profile Settings\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Profile Update Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-pencil-square me-2\"></i>Update Profile Information\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"profileForm\" (ngSubmit)=\"updateProfile()\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"firstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"firstName\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched\">\n @if (profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched) {\n <div class=\"invalid-feedback\">\n First name is required (max 30 characters)\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"lastName\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched\">\n @if (profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched) {\n <div class=\"invalid-feedback\">\n Last name is required (max 30 characters)\n </div>\n }\n </div>\n </div>\n <div class=\"mt-3\">\n <label for=\"currentPassword\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPassword\"\n formControlName=\"currentPassword\"\n placeholder=\"Enter your current password to confirm changes\"\n [class.is-invalid]=\"profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched\">\n @if (profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required to update profile\n </div>\n }\n </div>\n\n @if (profileMessage()) {\n <div class=\"alert alert-success mt-3\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ profileMessage() }}\n </div>\n }\n\n <div class=\"mt-3\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"profileForm.invalid || isUpdatingProfile()\">\n @if (isUpdatingProfile()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Update Profile\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'password'\">\n <button ngbNavLink (click)=\"setActiveTab('password')\">\n <i class=\"bi bi-shield-lock me-1\"></i>Change Password\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Password Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-shield-lock me-2\"></i>Change Password\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"passwordForm\" (ngSubmit)=\"changePassword()\">\n <div class=\"mb-3\">\n <label for=\"currentPasswordChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPasswordChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched\">\n @if (passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"newPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"newPassword\"\n formControlName=\"newPassword\"\n [class.is-invalid]=\"passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched\">\n @if (passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Password must be at least 8 characters long\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">Confirm New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"(passwordForm.get('confirmPassword')?.invalid || passwordForm.hasError('passwordMismatch')) && passwordForm.get('confirmPassword')?.touched\">\n @if (passwordForm.get('confirmPassword')?.invalid && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Please confirm your new password\n </div>\n }\n @if (passwordForm.hasError('passwordMismatch') && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Passwords do not match\n </div>\n }\n </div>\n\n @if (passwordMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ passwordMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"passwordForm.invalid || isChangingPassword()\">\n @if (isChangingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-check me-1\"></i>\n Change Password\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'email'\">\n <button ngbNavLink (click)=\"setActiveTab('email')\">\n <i class=\"bi bi-envelope me-1\"></i>Change Email\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Email Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Change Email Address\n </h6>\n </div>\n <div class=\"card-body\">\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Changing your email will require verification. You'll receive a confirmation email at your new address.\n </div>\n \n <form [formGroup]=\"emailChangeForm\" (ngSubmit)=\"requestEmailChange()\">\n <div class=\"mb-3\">\n <label for=\"currentEmail\" class=\"form-label\">Current Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"currentEmail\"\n [value]=\"currentUser()?.email\"\n readonly>\n </div>\n <div class=\"mb-3\">\n <label for=\"newEmail\" class=\"form-label\">New Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"newEmail\"\n formControlName=\"newEmail\"\n [class.is-invalid]=\"emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched\">\n @if (emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched) {\n <div class=\"invalid-feedback\">\n Please enter a valid email address\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"passwordEmailChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"passwordEmailChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched\">\n @if (emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n\n @if (emailMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ emailMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"emailChangeForm.invalid || isChangingEmail()\">\n @if (isChangingEmail()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-envelope-check me-1\"></i>\n Request Email Change\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n </ul>\n\n <div [ngbNavOutlet]=\"profileNav\" class=\"mt-3\"></div>\n\n <!-- Error Messages -->\n @if (errorMessage()) {\n <div class=\"alert alert-danger mt-3\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n </div>\n }\n }\n </div>\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "directive", type: i2$1.NgbNavContent, selector: "ng-template[ngbNavContent]" }, { kind: "directive", type: i2$1.NgbNav, selector: "[ngbNav]", inputs: ["activeId", "animation", "destroyOnHide", "orientation", "roles", "keyboard"], outputs: ["activeIdChange", "shown", "hidden", "navChange"], exportAs: ["ngbNav"] }, { kind: "directive", type: i2$1.NgbNavItem, selector: "[ngbNavItem]", inputs: ["destroyOnHide", "disabled", "domId", "ngbNavItem"], outputs: ["shown", "hidden"], exportAs: ["ngbNavItem"] }, { kind: "directive", type: i2$1.NgbNavItemRole, selector: "[ngbNavItem]:not(ng-container)" }, { kind: "directive", type: i2$1.NgbNavLinkButton, selector: "button[ngbNavLink]" }, { kind: "directive", type: i2$1.NgbNavLinkBase, selector: "[ngbNavLink]" }, { kind: "component", type: i2$1.NgbNavOutlet, selector: "[ngbNavOutlet]", inputs: ["paneRole", "ngbNavOutlet"] }] });
|
|
2950
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2951
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: UserProfileComponent, isStandalone: true, selector: "ccc-user-profile", ngImport: i0, template: "<div class=\"user-profile-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-person-circle me-2 text-primary\"></i>User Profile\n </h4>\n @if (isStaff()) {\n <span class=\"badge bg-warning ms-3\">Staff</span>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"profile-content p-4\">\n @if (isLoading()) {\n <div class=\"text-center\">\n <div class=\"spinner-border text-primary me-2\"></div>\n <span>Loading profile...</span>\n </div>\n } @else {\n <!-- User Info Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Account Information\n </h5>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Full Name:</small>\n <div class=\"fw-semibold\">{{ fullName() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Username:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.username }}</div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Email:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.email }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Account Status:</small>\n <div>\n @if (currentUser()?.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n </div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Member Since:</small>\n <div class=\"fw-semibold\">{{ joinDate() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Last Login:</small>\n <div class=\"fw-semibold\">{{ lastLogin() }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Navigation Tabs -->\n <ul ngbNav #profileNav=\"ngbNav\" [activeId]=\"activeTab()\" (activeIdChange)=\"setActiveTab($event)\" class=\"nav-tabs\">\n <li [ngbNavItem]=\"'profile'\">\n <button ngbNavLink (click)=\"setActiveTab('profile')\">\n <i class=\"bi bi-person me-1\"></i>Profile Settings\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Profile Update Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-pencil-square me-2\"></i>Update Profile Information\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"profileForm\" (ngSubmit)=\"updateProfile()\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"firstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"firstName\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched\">\n @if (profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched) {\n <div class=\"invalid-feedback\">\n First name is required (max 30 characters)\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"lastName\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched\">\n @if (profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched) {\n <div class=\"invalid-feedback\">\n Last name is required (max 30 characters)\n </div>\n }\n </div>\n </div>\n <div class=\"mt-3\">\n <label for=\"currentPassword\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPassword\"\n formControlName=\"currentPassword\"\n placeholder=\"Enter your current password to confirm changes\"\n [class.is-invalid]=\"profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched\">\n @if (profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required to update profile\n </div>\n }\n </div>\n\n @if (profileMessage()) {\n <div class=\"alert alert-success mt-3\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ profileMessage() }}\n </div>\n }\n\n <div class=\"mt-3\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"profileForm.invalid || isUpdatingProfile()\">\n @if (isUpdatingProfile()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Update Profile\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'password'\">\n <button ngbNavLink (click)=\"setActiveTab('password')\">\n <i class=\"bi bi-shield-lock me-1\"></i>Change Password\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Password Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-shield-lock me-2\"></i>Change Password\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"passwordForm\" (ngSubmit)=\"changePassword()\">\n <div class=\"mb-3\">\n <label for=\"currentPasswordChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPasswordChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched\">\n @if (passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"newPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"newPassword\"\n formControlName=\"newPassword\"\n [class.is-invalid]=\"passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched\">\n @if (passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Password must be at least 8 characters long\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">Confirm New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"(passwordForm.get('confirmPassword')?.invalid || passwordForm.hasError('passwordMismatch')) && passwordForm.get('confirmPassword')?.touched\">\n @if (passwordForm.get('confirmPassword')?.invalid && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Please confirm your new password\n </div>\n }\n @if (passwordForm.hasError('passwordMismatch') && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Passwords do not match\n </div>\n }\n </div>\n\n @if (passwordMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ passwordMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"passwordForm.invalid || isChangingPassword()\">\n @if (isChangingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-check me-1\"></i>\n Change Password\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'email'\">\n <button ngbNavLink (click)=\"setActiveTab('email')\">\n <i class=\"bi bi-envelope me-1\"></i>Change Email\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Email Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Change Email Address\n </h6>\n </div>\n <div class=\"card-body\">\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Changing your email will require verification. You'll receive a confirmation email at your new address.\n </div>\n \n <form [formGroup]=\"emailChangeForm\" (ngSubmit)=\"requestEmailChange()\">\n <div class=\"mb-3\">\n <label for=\"currentEmail\" class=\"form-label\">Current Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"currentEmail\"\n [value]=\"currentUser()?.email\"\n readonly>\n </div>\n <div class=\"mb-3\">\n <label for=\"newEmail\" class=\"form-label\">New Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"newEmail\"\n formControlName=\"newEmail\"\n [class.is-invalid]=\"emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched\">\n @if (emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched) {\n <div class=\"invalid-feedback\">\n Please enter a valid email address\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"passwordEmailChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"passwordEmailChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched\">\n @if (emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n\n @if (emailMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ emailMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"emailChangeForm.invalid || isChangingEmail()\">\n @if (isChangingEmail()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-envelope-check me-1\"></i>\n Request Email Change\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n </ul>\n\n <div [ngbNavOutlet]=\"profileNav\" class=\"mt-3\"></div>\n\n <!-- Error Messages -->\n @if (errorMessage()) {\n <div class=\"alert alert-danger mt-3\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n </div>\n }\n }\n </div>\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: NgbModule }, { kind: "directive", type: i2$1.NgbNavContent, selector: "ng-template[ngbNavContent]" }, { kind: "directive", type: i2$1.NgbNav, selector: "[ngbNav]", inputs: ["activeId", "animation", "destroyOnHide", "orientation", "roles", "keyboard"], outputs: ["activeIdChange", "shown", "hidden", "navChange"], exportAs: ["ngbNav"] }, { kind: "directive", type: i2$1.NgbNavItem, selector: "[ngbNavItem]", inputs: ["destroyOnHide", "disabled", "domId", "ngbNavItem"], outputs: ["shown", "hidden"], exportAs: ["ngbNavItem"] }, { kind: "directive", type: i2$1.NgbNavItemRole, selector: "[ngbNavItem]:not(ng-container)" }, { kind: "directive", type: i2$1.NgbNavLinkButton, selector: "button[ngbNavLink]" }, { kind: "directive", type: i2$1.NgbNavLinkBase, selector: "[ngbNavLink]" }, { kind: "component", type: i2$1.NgbNavOutlet, selector: "[ngbNavOutlet]", inputs: ["paneRole", "ngbNavOutlet"] }] });
|
|
2371
2952
|
}
|
|
2372
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
2953
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
2373
2954
|
type: Component,
|
|
2374
|
-
args: [{ selector: 'app-user-profile', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"user-profile-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-person-circle me-2 text-primary\"></i>User Profile\n </h4>\n @if (isStaff()) {\n <span class=\"badge bg-warning ms-3\">Staff</span>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"profile-content p-4\">\n @if (isLoading()) {\n <div class=\"text-center\">\n <div class=\"spinner-border text-primary me-2\"></div>\n <span>Loading profile...</span>\n </div>\n } @else {\n <!-- User Info Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Account Information\n </h5>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Full Name:</small>\n <div class=\"fw-semibold\">{{ fullName() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Username:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.username }}</div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Email:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.email }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Account Status:</small>\n <div>\n @if (currentUser()?.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n </div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Member Since:</small>\n <div class=\"fw-semibold\">{{ joinDate() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Last Login:</small>\n <div class=\"fw-semibold\">{{ lastLogin() }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Navigation Tabs -->\n <ul ngbNav #profileNav=\"ngbNav\" [activeId]=\"activeTab()\" (activeIdChange)=\"setActiveTab($event)\" class=\"nav-tabs\">\n <li [ngbNavItem]=\"'profile'\">\n <button ngbNavLink (click)=\"setActiveTab('profile')\">\n <i class=\"bi bi-person me-1\"></i>Profile Settings\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Profile Update Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-pencil-square me-2\"></i>Update Profile Information\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"profileForm\" (ngSubmit)=\"updateProfile()\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"firstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"firstName\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched\">\n @if (profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched) {\n <div class=\"invalid-feedback\">\n First name is required (max 30 characters)\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"lastName\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched\">\n @if (profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched) {\n <div class=\"invalid-feedback\">\n Last name is required (max 30 characters)\n </div>\n }\n </div>\n </div>\n <div class=\"mt-3\">\n <label for=\"currentPassword\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPassword\"\n formControlName=\"currentPassword\"\n placeholder=\"Enter your current password to confirm changes\"\n [class.is-invalid]=\"profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched\">\n @if (profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required to update profile\n </div>\n }\n </div>\n\n @if (profileMessage()) {\n <div class=\"alert alert-success mt-3\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ profileMessage() }}\n </div>\n }\n\n <div class=\"mt-3\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"profileForm.invalid || isUpdatingProfile()\">\n @if (isUpdatingProfile()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Update Profile\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'password'\">\n <button ngbNavLink (click)=\"setActiveTab('password')\">\n <i class=\"bi bi-shield-lock me-1\"></i>Change Password\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Password Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-shield-lock me-2\"></i>Change Password\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"passwordForm\" (ngSubmit)=\"changePassword()\">\n <div class=\"mb-3\">\n <label for=\"currentPasswordChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPasswordChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched\">\n @if (passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"newPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"newPassword\"\n formControlName=\"newPassword\"\n [class.is-invalid]=\"passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched\">\n @if (passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Password must be at least 8 characters long\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">Confirm New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"(passwordForm.get('confirmPassword')?.invalid || passwordForm.hasError('passwordMismatch')) && passwordForm.get('confirmPassword')?.touched\">\n @if (passwordForm.get('confirmPassword')?.invalid && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Please confirm your new password\n </div>\n }\n @if (passwordForm.hasError('passwordMismatch') && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Passwords do not match\n </div>\n }\n </div>\n\n @if (passwordMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ passwordMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"passwordForm.invalid || isChangingPassword()\">\n @if (isChangingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-check me-1\"></i>\n Change Password\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'email'\">\n <button ngbNavLink (click)=\"setActiveTab('email')\">\n <i class=\"bi bi-envelope me-1\"></i>Change Email\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Email Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Change Email Address\n </h6>\n </div>\n <div class=\"card-body\">\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Changing your email will require verification. You'll receive a confirmation email at your new address.\n </div>\n \n <form [formGroup]=\"emailChangeForm\" (ngSubmit)=\"requestEmailChange()\">\n <div class=\"mb-3\">\n <label for=\"currentEmail\" class=\"form-label\">Current Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"currentEmail\"\n [value]=\"currentUser()?.email\"\n readonly>\n </div>\n <div class=\"mb-3\">\n <label for=\"newEmail\" class=\"form-label\">New Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"newEmail\"\n formControlName=\"newEmail\"\n [class.is-invalid]=\"emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched\">\n @if (emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched) {\n <div class=\"invalid-feedback\">\n Please enter a valid email address\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"passwordEmailChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"passwordEmailChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched\">\n @if (emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n\n @if (emailMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ emailMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"emailChangeForm.invalid || isChangingEmail()\">\n @if (isChangingEmail()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-envelope-check me-1\"></i>\n Request Email Change\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n </ul>\n\n <div [ngbNavOutlet]=\"profileNav\" class=\"mt-3\"></div>\n\n <!-- Error Messages -->\n @if (errorMessage()) {\n <div class=\"alert alert-danger mt-3\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n </div>\n }\n }\n </div>\n</div>" }]
|
|
2955
|
+
args: [{ selector: 'ccc-user-profile', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbModule], template: "<div class=\"user-profile-container\">\n <!-- Header Section -->\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\n <div class=\"container-fluid\">\n <div class=\"d-flex align-items-center justify-content-between w-100\">\n <div class=\"d-flex align-items-center\">\n <h4 class=\"mb-0\">\n <i class=\"bi bi-person-circle me-2 text-primary\"></i>User Profile\n </h4>\n @if (isStaff()) {\n <span class=\"badge bg-warning ms-3\">Staff</span>\n }\n </div>\n </div>\n </div>\n </nav>\n\n <div class=\"profile-content p-4\">\n @if (isLoading()) {\n <div class=\"text-center\">\n <div class=\"spinner-border text-primary me-2\"></div>\n <span>Loading profile...</span>\n </div>\n } @else {\n <!-- User Info Card -->\n <div class=\"card mb-4\">\n <div class=\"card-header\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-info-circle me-2\"></i>Account Information\n </h5>\n </div>\n <div class=\"card-body\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Full Name:</small>\n <div class=\"fw-semibold\">{{ fullName() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Username:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.username }}</div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Email:</small>\n <div class=\"fw-semibold\">{{ currentUser()?.email }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Account Status:</small>\n <div>\n @if (currentUser()?.isActive) {\n <span class=\"badge bg-success\">Active</span>\n } @else {\n <span class=\"badge bg-danger\">Inactive</span>\n }\n </div>\n </div>\n </div>\n <div class=\"row mt-3\">\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Member Since:</small>\n <div class=\"fw-semibold\">{{ joinDate() }}</div>\n </div>\n <div class=\"col-md-6\">\n <small class=\"text-muted\">Last Login:</small>\n <div class=\"fw-semibold\">{{ lastLogin() }}</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Navigation Tabs -->\n <ul ngbNav #profileNav=\"ngbNav\" [activeId]=\"activeTab()\" (activeIdChange)=\"setActiveTab($event)\" class=\"nav-tabs\">\n <li [ngbNavItem]=\"'profile'\">\n <button ngbNavLink (click)=\"setActiveTab('profile')\">\n <i class=\"bi bi-person me-1\"></i>Profile Settings\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Profile Update Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-pencil-square me-2\"></i>Update Profile Information\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"profileForm\" (ngSubmit)=\"updateProfile()\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <label for=\"firstName\" class=\"form-label\">First Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"firstName\"\n formControlName=\"firstName\"\n [class.is-invalid]=\"profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched\">\n @if (profileForm.get('firstName')?.invalid && profileForm.get('firstName')?.touched) {\n <div class=\"invalid-feedback\">\n First name is required (max 30 characters)\n </div>\n }\n </div>\n <div class=\"col-md-6\">\n <label for=\"lastName\" class=\"form-label\">Last Name</label>\n <input\n type=\"text\"\n class=\"form-control\"\n id=\"lastName\"\n formControlName=\"lastName\"\n [class.is-invalid]=\"profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched\">\n @if (profileForm.get('lastName')?.invalid && profileForm.get('lastName')?.touched) {\n <div class=\"invalid-feedback\">\n Last name is required (max 30 characters)\n </div>\n }\n </div>\n </div>\n <div class=\"mt-3\">\n <label for=\"currentPassword\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPassword\"\n formControlName=\"currentPassword\"\n placeholder=\"Enter your current password to confirm changes\"\n [class.is-invalid]=\"profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched\">\n @if (profileForm.get('currentPassword')?.invalid && profileForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required to update profile\n </div>\n }\n </div>\n\n @if (profileMessage()) {\n <div class=\"alert alert-success mt-3\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ profileMessage() }}\n </div>\n }\n\n <div class=\"mt-3\">\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"profileForm.invalid || isUpdatingProfile()\">\n @if (isUpdatingProfile()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-check-circle me-1\"></i>\n Update Profile\n </button>\n </div>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'password'\">\n <button ngbNavLink (click)=\"setActiveTab('password')\">\n <i class=\"bi bi-shield-lock me-1\"></i>Change Password\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Password Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-shield-lock me-2\"></i>Change Password\n </h6>\n </div>\n <div class=\"card-body\">\n <form [formGroup]=\"passwordForm\" (ngSubmit)=\"changePassword()\">\n <div class=\"mb-3\">\n <label for=\"currentPasswordChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"currentPasswordChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched\">\n @if (passwordForm.get('currentPassword')?.invalid && passwordForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"newPassword\" class=\"form-label\">New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"newPassword\"\n formControlName=\"newPassword\"\n [class.is-invalid]=\"passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched\">\n @if (passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Password must be at least 8 characters long\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"confirmPassword\" class=\"form-label\">Confirm New Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"confirmPassword\"\n formControlName=\"confirmPassword\"\n [class.is-invalid]=\"(passwordForm.get('confirmPassword')?.invalid || passwordForm.hasError('passwordMismatch')) && passwordForm.get('confirmPassword')?.touched\">\n @if (passwordForm.get('confirmPassword')?.invalid && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Please confirm your new password\n </div>\n }\n @if (passwordForm.hasError('passwordMismatch') && passwordForm.get('confirmPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Passwords do not match\n </div>\n }\n </div>\n\n @if (passwordMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ passwordMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"passwordForm.invalid || isChangingPassword()\">\n @if (isChangingPassword()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-shield-check me-1\"></i>\n Change Password\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n\n <li [ngbNavItem]=\"'email'\">\n <button ngbNavLink (click)=\"setActiveTab('email')\">\n <i class=\"bi bi-envelope me-1\"></i>Change Email\n </button>\n <ng-template ngbNavContent>\n <div class=\"mt-4\">\n <!-- Email Change Form -->\n <div class=\"card\">\n <div class=\"card-header\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-envelope me-2\"></i>Change Email Address\n </h6>\n </div>\n <div class=\"card-body\">\n <div class=\"alert alert-info\">\n <i class=\"bi bi-info-circle me-2\"></i>\n Changing your email will require verification. You'll receive a confirmation email at your new address.\n </div>\n \n <form [formGroup]=\"emailChangeForm\" (ngSubmit)=\"requestEmailChange()\">\n <div class=\"mb-3\">\n <label for=\"currentEmail\" class=\"form-label\">Current Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"currentEmail\"\n [value]=\"currentUser()?.email\"\n readonly>\n </div>\n <div class=\"mb-3\">\n <label for=\"newEmail\" class=\"form-label\">New Email</label>\n <input\n type=\"email\"\n class=\"form-control\"\n id=\"newEmail\"\n formControlName=\"newEmail\"\n [class.is-invalid]=\"emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched\">\n @if (emailChangeForm.get('newEmail')?.invalid && emailChangeForm.get('newEmail')?.touched) {\n <div class=\"invalid-feedback\">\n Please enter a valid email address\n </div>\n }\n </div>\n <div class=\"mb-3\">\n <label for=\"passwordEmailChange\" class=\"form-label\">Current Password</label>\n <input\n type=\"password\"\n class=\"form-control\"\n id=\"passwordEmailChange\"\n formControlName=\"currentPassword\"\n [class.is-invalid]=\"emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched\">\n @if (emailChangeForm.get('currentPassword')?.invalid && emailChangeForm.get('currentPassword')?.touched) {\n <div class=\"invalid-feedback\">\n Current password is required\n </div>\n }\n </div>\n\n @if (emailMessage()) {\n <div class=\"alert alert-success\">\n <i class=\"bi bi-check-circle me-2\"></i>{{ emailMessage() }}\n </div>\n }\n\n <button\n type=\"submit\"\n class=\"btn btn-primary\"\n [disabled]=\"emailChangeForm.invalid || isChangingEmail()\">\n @if (isChangingEmail()) {\n <span class=\"spinner-border spinner-border-sm me-2\"></span>\n }\n <i class=\"bi bi-envelope-check me-1\"></i>\n Request Email Change\n </button>\n </form>\n </div>\n </div>\n </div>\n </ng-template>\n </li>\n </ul>\n\n <div [ngbNavOutlet]=\"profileNav\" class=\"mt-3\"></div>\n\n <!-- Error Messages -->\n @if (errorMessage()) {\n <div class=\"alert alert-danger mt-3\">\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ errorMessage() }}\n </div>\n }\n }\n </div>\n</div>" }]
|
|
2375
2956
|
}], ctorParameters: () => [] });
|
|
2376
2957
|
|
|
2377
2958
|
class SiteConfigComponent {
|
|
@@ -2542,64 +3123,70 @@ class SiteConfigComponent {
|
|
|
2542
3123
|
fileInput.value = '';
|
|
2543
3124
|
}
|
|
2544
3125
|
}
|
|
2545
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
2546
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: SiteConfigComponent, isStandalone: true, selector: "app-site-config", ngImport: i0, template: "<div class=\"site-config-container\">\r\n <!-- Header Section -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100\">\r\n <div class=\"d-flex align-items-center\">\r\n <h4 class=\"navbar-brand m-0 fw-bold\">\r\n <i class=\"bi bi-gear me-2\"></i>Site Configuration\r\n </h4>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button \r\n type=\"submit\" \r\n form=\"configForm\"\r\n class=\"btn btn-primary btn-sm\"\r\n [disabled]=\"configForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-check-lg me-1\"></i>\r\n }\r\n Save Configuration\r\n </button>\r\n \r\n <button \r\n type=\"button\" \r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"resetForm()\"\r\n [disabled]=\"loading()\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n Reset\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <div class=\"config-content p-4\">\r\n <div class=\"row\">\r\n <div class=\"col-lg-8\">\r\n <!-- Site Branding Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-brush me-2\"></i>Site Branding\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <form id=\"configForm\" [formGroup]=\"configForm\" (ngSubmit)=\"onSubmit()\">\r\n <!-- Site Name -->\r\n <div class=\"mb-3\">\r\n <label for=\"site_name\" class=\"form-label\">\r\n <i class=\"bi bi-building me-1\"></i>\r\n Site Name <span class=\"text-danger\">*</span>\r\n </label>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"site_name\"\r\n formControlName=\"siteName\"\r\n [class.is-invalid]=\"configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched\"\r\n placeholder=\"Enter your site name\">\r\n @if (configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('siteName')?.errors?.['required']) {\r\n <span>Site name is required</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['minlength']) {\r\n <span>Site name must be at least 1 character</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['maxlength']) {\r\n <span>Site name must be less than 255 characters</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n This will appear in the navigation bar and login page\r\n </div>\r\n </div>\r\n\r\n <!-- Logo URL -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_url\" class=\"form-label\">\r\n <i class=\"bi bi-image me-1\"></i>\r\n Logo URL\r\n </label>\r\n <input \r\n type=\"url\" \r\n class=\"form-control\" \r\n id=\"logo_url\"\r\n formControlName=\"logoUrl\"\r\n placeholder=\"https://example.com/logo.png\">\r\n <div class=\"form-text\">\r\n Optional: URL to your organization's logo\r\n </div>\r\n </div>\r\n\r\n <!-- Logo File Upload -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_file\" class=\"form-label\">\r\n <i class=\"bi bi-upload me-1\"></i>\r\n Upload Logo File\r\n </label>\r\n <input \r\n type=\"file\" \r\n class=\"form-control\" \r\n id=\"logo_file\"\r\n accept=\"image/*\"\r\n (change)=\"onLogoFileSelected($event)\">\r\n <div class=\"form-text\">\r\n Upload a logo file (overrides logo URL). Supported: JPEG, PNG, GIF, SVG. Max 5MB.\r\n </div>\r\n @if (selectedLogoFile()) {\r\n <div class=\"mt-2\">\r\n <div class=\"alert alert-info py-2\">\r\n <i class=\"bi bi-file-image me-1\"></i>\r\n Selected: {{ selectedLogoFile()?.name }}\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger ms-2\" (click)=\"clearLogoFile()\">\r\n <i class=\"bi bi-x\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Primary Color -->\r\n <div class=\"mb-3\">\r\n <label for=\"primary_color\" class=\"form-label\">\r\n <i class=\"bi bi-palette me-1\"></i>\r\n Primary Color\r\n </label>\r\n <div class=\"color-picker-wrapper\">\r\n <color-sketch\r\n [color]=\"configForm.get('primaryColor')?.value\"\r\n (onChange)=\"onColorChange($event)\"\r\n [presetColors]=\"presetColors\">\r\n </color-sketch>\r\n </div>\r\n @if (configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('primaryColor')?.errors?.['pattern']) {\r\n <span>Please enter a valid hex color (e.g., #1976d2)</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n Main theme color for navigation and buttons\r\n </div>\r\n </div>\r\n\r\n <!-- Show Powered By -->\r\n <div class=\"mb-4\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"show_powered_by\"\r\n formControlName=\"showPoweredBy\">\r\n <label class=\"form-check-label\" for=\"show_powered_by\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Show \"Powered by CUPCAKE Vanilla\"\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Display a small footer credit (appears on hover in bottom-right corner)\r\n </div>\r\n </div>\r\n\r\n <!-- Authentication Settings Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-shield-lock me-2\"></i>Authentication Settings\r\n </h6>\r\n \r\n <!-- Allow User Registration -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"allow_user_registration\"\r\n formControlName=\"allowUserRegistration\">\r\n <label class=\"form-check-label\" for=\"allow_user_registration\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Allow Public User Registration\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n When enabled, users can register new accounts without admin approval. \r\n The registration API endpoint will be accessible to the public.\r\n </div>\r\n </div>\r\n\r\n <!-- Enable ORCID Login -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"enable_orcid_login\"\r\n formControlName=\"enableOrcidLogin\">\r\n <label class=\"form-check-label\" for=\"enable_orcid_login\">\r\n <i class=\"bi bi-person-badge me-1\"></i>\r\n Enable ORCID OAuth Login\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Allow users to log in using their ORCID account. \r\n Requires ORCID OAuth configuration in server settings.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n </form>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n\r\n <div class=\"col-lg-4\">\r\n <!-- Preview Panel -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-eye me-2\"></i>Preview\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"preview-navbar mb-3\" \r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <div class=\"preview-nav\">\r\n <span class=\"preview-brand\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 20px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"preview-login-card\">\r\n <h6 class=\"text-center mb-3\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 24px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </h6>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </div>\r\n }\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Regular Login\r\n </div>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <div class=\"text-center mb-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Don't have an account? <a href=\"#\" class=\"text-decoration-none\">Register here</a>\r\n </small>\r\n </div>\r\n }\r\n <div class=\"text-center\">\r\n <small class=\"text-muted\">\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }} - Scientific Metadata Management\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mt-3\">\r\n <h6 class=\"text-muted\">Authentication Status:</h6>\r\n <ul class=\"list-unstyled small text-muted mb-0\">\r\n <li>\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Regular login: Always enabled\r\n </li>\r\n <li>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n ORCID login: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n ORCID login: Disabled\r\n }\r\n </li>\r\n <li>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Public registration: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n Public registration: Disabled\r\n }\r\n </li>\r\n </ul>\r\n </div>\r\n\r\n @if (configForm.get('showPoweredBy')?.value) {\r\n <div class=\"mt-3\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Footer: \"Powered by CUPCAKE Vanilla\" will appear on hover\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".color-picker-wrapper ::ng-deep .sketch-picker{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;box-shadow:0 .5rem 1rem #00000026!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-controls .sketch-picker-color-wrap .sketch-picker-active{border:2px solid var(--bs-border-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields{background:var(--bs-body-bg)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;color:var(--bs-body-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input:focus{border-color:var(--bs-primary)!important;box-shadow:0 0 0 .25rem rgba(var(--bs-primary-rgb),.25)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field label{color:var(--bs-body-color)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }, { kind: "ngmodule", type: ColorSketchModule }, { kind: "component", type: i2$2.SketchComponent, selector: "color-sketch", inputs: ["disableAlpha", "presetColors", "width"] }] });
|
|
3126
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SiteConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3127
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: SiteConfigComponent, isStandalone: true, selector: "ccc-site-config", ngImport: i0, template: "<div class=\"site-config-container\">\r\n <!-- Header Section -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100\">\r\n <div class=\"d-flex align-items-center\">\r\n <h4 class=\"navbar-brand m-0 fw-bold\">\r\n <i class=\"bi bi-gear me-2\"></i>Site Configuration\r\n </h4>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button \r\n type=\"submit\" \r\n form=\"configForm\"\r\n class=\"btn btn-primary btn-sm\"\r\n [disabled]=\"configForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-check-lg me-1\"></i>\r\n }\r\n Save Configuration\r\n </button>\r\n \r\n <button \r\n type=\"button\" \r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"resetForm()\"\r\n [disabled]=\"loading()\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n Reset\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <div class=\"config-content p-4\">\r\n <div class=\"row\">\r\n <div class=\"col-lg-8\">\r\n <!-- Site Branding Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-brush me-2\"></i>Site Branding\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <form id=\"configForm\" [formGroup]=\"configForm\" (ngSubmit)=\"onSubmit()\">\r\n <!-- Site Name -->\r\n <div class=\"mb-3\">\r\n <label for=\"site_name\" class=\"form-label\">\r\n <i class=\"bi bi-building me-1\"></i>\r\n Site Name <span class=\"text-danger\">*</span>\r\n </label>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"site_name\"\r\n formControlName=\"siteName\"\r\n [class.is-invalid]=\"configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched\"\r\n placeholder=\"Enter your site name\">\r\n @if (configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('siteName')?.errors?.['required']) {\r\n <span>Site name is required</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['minlength']) {\r\n <span>Site name must be at least 1 character</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['maxlength']) {\r\n <span>Site name must be less than 255 characters</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n This will appear in the navigation bar and login page\r\n </div>\r\n </div>\r\n\r\n <!-- Logo URL -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_url\" class=\"form-label\">\r\n <i class=\"bi bi-image me-1\"></i>\r\n Logo URL\r\n </label>\r\n <input \r\n type=\"url\" \r\n class=\"form-control\" \r\n id=\"logo_url\"\r\n formControlName=\"logoUrl\"\r\n placeholder=\"https://example.com/logo.png\">\r\n <div class=\"form-text\">\r\n Optional: URL to your organization's logo\r\n </div>\r\n </div>\r\n\r\n <!-- Logo File Upload -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_file\" class=\"form-label\">\r\n <i class=\"bi bi-upload me-1\"></i>\r\n Upload Logo File\r\n </label>\r\n <input \r\n type=\"file\" \r\n class=\"form-control\" \r\n id=\"logo_file\"\r\n accept=\"image/*\"\r\n (change)=\"onLogoFileSelected($event)\">\r\n <div class=\"form-text\">\r\n Upload a logo file (overrides logo URL). Supported: JPEG, PNG, GIF, SVG. Max 5MB.\r\n </div>\r\n @if (selectedLogoFile()) {\r\n <div class=\"mt-2\">\r\n <div class=\"alert alert-info py-2\">\r\n <i class=\"bi bi-file-image me-1\"></i>\r\n Selected: {{ selectedLogoFile()?.name }}\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger ms-2\" (click)=\"clearLogoFile()\">\r\n <i class=\"bi bi-x\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Primary Color -->\r\n <div class=\"mb-3\">\r\n <label for=\"primary_color\" class=\"form-label\">\r\n <i class=\"bi bi-palette me-1\"></i>\r\n Primary Color\r\n </label>\r\n <div class=\"color-picker-wrapper\">\r\n <color-sketch\r\n [color]=\"configForm.get('primaryColor')?.value\"\r\n (onChange)=\"onColorChange($event)\"\r\n [presetColors]=\"presetColors\">\r\n </color-sketch>\r\n </div>\r\n @if (configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('primaryColor')?.errors?.['pattern']) {\r\n <span>Please enter a valid hex color (e.g., #1976d2)</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n Main theme color for navigation and buttons\r\n </div>\r\n </div>\r\n\r\n <!-- Show Powered By -->\r\n <div class=\"mb-4\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"show_powered_by\"\r\n formControlName=\"showPoweredBy\">\r\n <label class=\"form-check-label\" for=\"show_powered_by\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Show \"Powered by CUPCAKE Vanilla\"\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Display a small footer credit (appears on hover in bottom-right corner)\r\n </div>\r\n </div>\r\n\r\n <!-- Authentication Settings Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-shield-lock me-2\"></i>Authentication Settings\r\n </h6>\r\n \r\n <!-- Allow User Registration -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"allow_user_registration\"\r\n formControlName=\"allowUserRegistration\">\r\n <label class=\"form-check-label\" for=\"allow_user_registration\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Allow Public User Registration\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n When enabled, users can register new accounts without admin approval. \r\n The registration API endpoint will be accessible to the public.\r\n </div>\r\n </div>\r\n\r\n <!-- Enable ORCID Login -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"enable_orcid_login\"\r\n formControlName=\"enableOrcidLogin\">\r\n <label class=\"form-check-label\" for=\"enable_orcid_login\">\r\n <i class=\"bi bi-person-badge me-1\"></i>\r\n Enable ORCID OAuth Login\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Allow users to log in using their ORCID account. \r\n Requires ORCID OAuth configuration in server settings.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n </form>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n\r\n <div class=\"col-lg-4\">\r\n <!-- Preview Panel -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-eye me-2\"></i>Preview\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"preview-navbar mb-3\" \r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <div class=\"preview-nav\">\r\n <span class=\"preview-brand\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 20px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"preview-login-card\">\r\n <h6 class=\"text-center mb-3\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 24px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </h6>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </div>\r\n }\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Regular Login\r\n </div>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <div class=\"text-center mb-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Don't have an account? <a href=\"#\" class=\"text-decoration-none\">Register here</a>\r\n </small>\r\n </div>\r\n }\r\n <div class=\"text-center\">\r\n <small class=\"text-muted\">\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }} - Scientific Metadata Management\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mt-3\">\r\n <h6 class=\"text-muted\">Authentication Status:</h6>\r\n <ul class=\"list-unstyled small text-muted mb-0\">\r\n <li>\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Regular login: Always enabled\r\n </li>\r\n <li>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n ORCID login: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n ORCID login: Disabled\r\n }\r\n </li>\r\n <li>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Public registration: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n Public registration: Disabled\r\n }\r\n </li>\r\n </ul>\r\n </div>\r\n\r\n @if (configForm.get('showPoweredBy')?.value) {\r\n <div class=\"mt-3\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Footer: \"Powered by CUPCAKE Vanilla\" will appear on hover\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".color-picker-wrapper ::ng-deep .sketch-picker{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;box-shadow:0 .5rem 1rem #00000026!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-controls .sketch-picker-color-wrap .sketch-picker-active{border:2px solid var(--bs-border-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields{background:var(--bs-body-bg)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;color:var(--bs-body-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input:focus{border-color:var(--bs-primary)!important;box-shadow:0 0 0 .25rem rgba(var(--bs-primary-rgb),.25)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field label{color:var(--bs-body-color)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }, { kind: "ngmodule", type: ColorSketchModule }, { kind: "component", type: i2$2.SketchComponent, selector: "color-sketch", inputs: ["disableAlpha", "presetColors", "width"] }] });
|
|
2547
3128
|
}
|
|
2548
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
3129
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SiteConfigComponent, decorators: [{
|
|
2549
3130
|
type: Component,
|
|
2550
|
-
args: [{ selector: 'app-site-config', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert, ColorSketchModule], template: "<div class=\"site-config-container\">\r\n <!-- Header Section -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100\">\r\n <div class=\"d-flex align-items-center\">\r\n <h4 class=\"navbar-brand m-0 fw-bold\">\r\n <i class=\"bi bi-gear me-2\"></i>Site Configuration\r\n </h4>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button \r\n type=\"submit\" \r\n form=\"configForm\"\r\n class=\"btn btn-primary btn-sm\"\r\n [disabled]=\"configForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-check-lg me-1\"></i>\r\n }\r\n Save Configuration\r\n </button>\r\n \r\n <button \r\n type=\"button\" \r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"resetForm()\"\r\n [disabled]=\"loading()\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n Reset\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <div class=\"config-content p-4\">\r\n <div class=\"row\">\r\n <div class=\"col-lg-8\">\r\n <!-- Site Branding Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-brush me-2\"></i>Site Branding\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <form id=\"configForm\" [formGroup]=\"configForm\" (ngSubmit)=\"onSubmit()\">\r\n <!-- Site Name -->\r\n <div class=\"mb-3\">\r\n <label for=\"site_name\" class=\"form-label\">\r\n <i class=\"bi bi-building me-1\"></i>\r\n Site Name <span class=\"text-danger\">*</span>\r\n </label>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"site_name\"\r\n formControlName=\"siteName\"\r\n [class.is-invalid]=\"configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched\"\r\n placeholder=\"Enter your site name\">\r\n @if (configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('siteName')?.errors?.['required']) {\r\n <span>Site name is required</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['minlength']) {\r\n <span>Site name must be at least 1 character</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['maxlength']) {\r\n <span>Site name must be less than 255 characters</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n This will appear in the navigation bar and login page\r\n </div>\r\n </div>\r\n\r\n <!-- Logo URL -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_url\" class=\"form-label\">\r\n <i class=\"bi bi-image me-1\"></i>\r\n Logo URL\r\n </label>\r\n <input \r\n type=\"url\" \r\n class=\"form-control\" \r\n id=\"logo_url\"\r\n formControlName=\"logoUrl\"\r\n placeholder=\"https://example.com/logo.png\">\r\n <div class=\"form-text\">\r\n Optional: URL to your organization's logo\r\n </div>\r\n </div>\r\n\r\n <!-- Logo File Upload -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_file\" class=\"form-label\">\r\n <i class=\"bi bi-upload me-1\"></i>\r\n Upload Logo File\r\n </label>\r\n <input \r\n type=\"file\" \r\n class=\"form-control\" \r\n id=\"logo_file\"\r\n accept=\"image/*\"\r\n (change)=\"onLogoFileSelected($event)\">\r\n <div class=\"form-text\">\r\n Upload a logo file (overrides logo URL). Supported: JPEG, PNG, GIF, SVG. Max 5MB.\r\n </div>\r\n @if (selectedLogoFile()) {\r\n <div class=\"mt-2\">\r\n <div class=\"alert alert-info py-2\">\r\n <i class=\"bi bi-file-image me-1\"></i>\r\n Selected: {{ selectedLogoFile()?.name }}\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger ms-2\" (click)=\"clearLogoFile()\">\r\n <i class=\"bi bi-x\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Primary Color -->\r\n <div class=\"mb-3\">\r\n <label for=\"primary_color\" class=\"form-label\">\r\n <i class=\"bi bi-palette me-1\"></i>\r\n Primary Color\r\n </label>\r\n <div class=\"color-picker-wrapper\">\r\n <color-sketch\r\n [color]=\"configForm.get('primaryColor')?.value\"\r\n (onChange)=\"onColorChange($event)\"\r\n [presetColors]=\"presetColors\">\r\n </color-sketch>\r\n </div>\r\n @if (configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('primaryColor')?.errors?.['pattern']) {\r\n <span>Please enter a valid hex color (e.g., #1976d2)</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n Main theme color for navigation and buttons\r\n </div>\r\n </div>\r\n\r\n <!-- Show Powered By -->\r\n <div class=\"mb-4\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"show_powered_by\"\r\n formControlName=\"showPoweredBy\">\r\n <label class=\"form-check-label\" for=\"show_powered_by\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Show \"Powered by CUPCAKE Vanilla\"\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Display a small footer credit (appears on hover in bottom-right corner)\r\n </div>\r\n </div>\r\n\r\n <!-- Authentication Settings Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-shield-lock me-2\"></i>Authentication Settings\r\n </h6>\r\n \r\n <!-- Allow User Registration -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"allow_user_registration\"\r\n formControlName=\"allowUserRegistration\">\r\n <label class=\"form-check-label\" for=\"allow_user_registration\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Allow Public User Registration\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n When enabled, users can register new accounts without admin approval. \r\n The registration API endpoint will be accessible to the public.\r\n </div>\r\n </div>\r\n\r\n <!-- Enable ORCID Login -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"enable_orcid_login\"\r\n formControlName=\"enableOrcidLogin\">\r\n <label class=\"form-check-label\" for=\"enable_orcid_login\">\r\n <i class=\"bi bi-person-badge me-1\"></i>\r\n Enable ORCID OAuth Login\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Allow users to log in using their ORCID account. \r\n Requires ORCID OAuth configuration in server settings.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n </form>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n\r\n <div class=\"col-lg-4\">\r\n <!-- Preview Panel -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-eye me-2\"></i>Preview\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"preview-navbar mb-3\" \r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <div class=\"preview-nav\">\r\n <span class=\"preview-brand\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 20px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"preview-login-card\">\r\n <h6 class=\"text-center mb-3\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 24px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </h6>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </div>\r\n }\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Regular Login\r\n </div>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <div class=\"text-center mb-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Don't have an account? <a href=\"#\" class=\"text-decoration-none\">Register here</a>\r\n </small>\r\n </div>\r\n }\r\n <div class=\"text-center\">\r\n <small class=\"text-muted\">\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }} - Scientific Metadata Management\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mt-3\">\r\n <h6 class=\"text-muted\">Authentication Status:</h6>\r\n <ul class=\"list-unstyled small text-muted mb-0\">\r\n <li>\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Regular login: Always enabled\r\n </li>\r\n <li>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n ORCID login: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n ORCID login: Disabled\r\n }\r\n </li>\r\n <li>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Public registration: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n Public registration: Disabled\r\n }\r\n </li>\r\n </ul>\r\n </div>\r\n\r\n @if (configForm.get('showPoweredBy')?.value) {\r\n <div class=\"mt-3\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Footer: \"Powered by CUPCAKE Vanilla\" will appear on hover\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".color-picker-wrapper ::ng-deep .sketch-picker{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;box-shadow:0 .5rem 1rem #00000026!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-controls .sketch-picker-color-wrap .sketch-picker-active{border:2px solid var(--bs-border-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields{background:var(--bs-body-bg)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;color:var(--bs-body-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input:focus{border-color:var(--bs-primary)!important;box-shadow:0 0 0 .25rem rgba(var(--bs-primary-rgb),.25)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field label{color:var(--bs-body-color)!important}\n"] }]
|
|
3131
|
+
args: [{ selector: 'ccc-site-config', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert, ColorSketchModule], template: "<div class=\"site-config-container\">\r\n <!-- Header Section -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100\">\r\n <div class=\"d-flex align-items-center\">\r\n <h4 class=\"navbar-brand m-0 fw-bold\">\r\n <i class=\"bi bi-gear me-2\"></i>Site Configuration\r\n </h4>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button \r\n type=\"submit\" \r\n form=\"configForm\"\r\n class=\"btn btn-primary btn-sm\"\r\n [disabled]=\"configForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-check-lg me-1\"></i>\r\n }\r\n Save Configuration\r\n </button>\r\n \r\n <button \r\n type=\"button\" \r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"resetForm()\"\r\n [disabled]=\"loading()\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n Reset\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <div class=\"config-content p-4\">\r\n <div class=\"row\">\r\n <div class=\"col-lg-8\">\r\n <!-- Site Branding Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-brush me-2\"></i>Site Branding\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <form id=\"configForm\" [formGroup]=\"configForm\" (ngSubmit)=\"onSubmit()\">\r\n <!-- Site Name -->\r\n <div class=\"mb-3\">\r\n <label for=\"site_name\" class=\"form-label\">\r\n <i class=\"bi bi-building me-1\"></i>\r\n Site Name <span class=\"text-danger\">*</span>\r\n </label>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"site_name\"\r\n formControlName=\"siteName\"\r\n [class.is-invalid]=\"configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched\"\r\n placeholder=\"Enter your site name\">\r\n @if (configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('siteName')?.errors?.['required']) {\r\n <span>Site name is required</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['minlength']) {\r\n <span>Site name must be at least 1 character</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['maxlength']) {\r\n <span>Site name must be less than 255 characters</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n This will appear in the navigation bar and login page\r\n </div>\r\n </div>\r\n\r\n <!-- Logo URL -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_url\" class=\"form-label\">\r\n <i class=\"bi bi-image me-1\"></i>\r\n Logo URL\r\n </label>\r\n <input \r\n type=\"url\" \r\n class=\"form-control\" \r\n id=\"logo_url\"\r\n formControlName=\"logoUrl\"\r\n placeholder=\"https://example.com/logo.png\">\r\n <div class=\"form-text\">\r\n Optional: URL to your organization's logo\r\n </div>\r\n </div>\r\n\r\n <!-- Logo File Upload -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_file\" class=\"form-label\">\r\n <i class=\"bi bi-upload me-1\"></i>\r\n Upload Logo File\r\n </label>\r\n <input \r\n type=\"file\" \r\n class=\"form-control\" \r\n id=\"logo_file\"\r\n accept=\"image/*\"\r\n (change)=\"onLogoFileSelected($event)\">\r\n <div class=\"form-text\">\r\n Upload a logo file (overrides logo URL). Supported: JPEG, PNG, GIF, SVG. Max 5MB.\r\n </div>\r\n @if (selectedLogoFile()) {\r\n <div class=\"mt-2\">\r\n <div class=\"alert alert-info py-2\">\r\n <i class=\"bi bi-file-image me-1\"></i>\r\n Selected: {{ selectedLogoFile()?.name }}\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger ms-2\" (click)=\"clearLogoFile()\">\r\n <i class=\"bi bi-x\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Primary Color -->\r\n <div class=\"mb-3\">\r\n <label for=\"primary_color\" class=\"form-label\">\r\n <i class=\"bi bi-palette me-1\"></i>\r\n Primary Color\r\n </label>\r\n <div class=\"color-picker-wrapper\">\r\n <color-sketch\r\n [color]=\"configForm.get('primaryColor')?.value\"\r\n (onChange)=\"onColorChange($event)\"\r\n [presetColors]=\"presetColors\">\r\n </color-sketch>\r\n </div>\r\n @if (configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('primaryColor')?.errors?.['pattern']) {\r\n <span>Please enter a valid hex color (e.g., #1976d2)</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n Main theme color for navigation and buttons\r\n </div>\r\n </div>\r\n\r\n <!-- Show Powered By -->\r\n <div class=\"mb-4\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"show_powered_by\"\r\n formControlName=\"showPoweredBy\">\r\n <label class=\"form-check-label\" for=\"show_powered_by\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Show \"Powered by CUPCAKE Vanilla\"\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Display a small footer credit (appears on hover in bottom-right corner)\r\n </div>\r\n </div>\r\n\r\n <!-- Authentication Settings Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-shield-lock me-2\"></i>Authentication Settings\r\n </h6>\r\n \r\n <!-- Allow User Registration -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"allow_user_registration\"\r\n formControlName=\"allowUserRegistration\">\r\n <label class=\"form-check-label\" for=\"allow_user_registration\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Allow Public User Registration\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n When enabled, users can register new accounts without admin approval. \r\n The registration API endpoint will be accessible to the public.\r\n </div>\r\n </div>\r\n\r\n <!-- Enable ORCID Login -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"enable_orcid_login\"\r\n formControlName=\"enableOrcidLogin\">\r\n <label class=\"form-check-label\" for=\"enable_orcid_login\">\r\n <i class=\"bi bi-person-badge me-1\"></i>\r\n Enable ORCID OAuth Login\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Allow users to log in using their ORCID account. \r\n Requires ORCID OAuth configuration in server settings.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n </form>\r\n </div>\r\n </div>\r\n\r\n </div>\r\n\r\n <div class=\"col-lg-4\">\r\n <!-- Preview Panel -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-eye me-2\"></i>Preview\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"preview-navbar mb-3\" \r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <div class=\"preview-nav\">\r\n <span class=\"preview-brand\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 20px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"preview-login-card\">\r\n <h6 class=\"text-center mb-3\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 24px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </h6>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </div>\r\n }\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Regular Login\r\n </div>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <div class=\"text-center mb-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Don't have an account? <a href=\"#\" class=\"text-decoration-none\">Register here</a>\r\n </small>\r\n </div>\r\n }\r\n <div class=\"text-center\">\r\n <small class=\"text-muted\">\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }} - Scientific Metadata Management\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mt-3\">\r\n <h6 class=\"text-muted\">Authentication Status:</h6>\r\n <ul class=\"list-unstyled small text-muted mb-0\">\r\n <li>\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Regular login: Always enabled\r\n </li>\r\n <li>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n ORCID login: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n ORCID login: Disabled\r\n }\r\n </li>\r\n <li>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Public registration: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n Public registration: Disabled\r\n }\r\n </li>\r\n </ul>\r\n </div>\r\n\r\n @if (configForm.get('showPoweredBy')?.value) {\r\n <div class=\"mt-3\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Footer: \"Powered by CUPCAKE Vanilla\" will appear on hover\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>", styles: [".color-picker-wrapper ::ng-deep .sketch-picker{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;box-shadow:0 .5rem 1rem #00000026!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-controls .sketch-picker-color-wrap .sketch-picker-active{border:2px solid var(--bs-border-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields{background:var(--bs-body-bg)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input{background:var(--bs-body-bg)!important;border:1px solid var(--bs-border-color)!important;color:var(--bs-body-color)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field input:focus{border-color:var(--bs-primary)!important;box-shadow:0 0 0 .25rem rgba(var(--bs-primary-rgb),.25)!important}.color-picker-wrapper ::ng-deep .sketch-picker .sketch-picker-fields .sketch-picker-field label{color:var(--bs-body-color)!important}\n"] }]
|
|
2551
3132
|
}], ctorParameters: () => [] });
|
|
2552
3133
|
|
|
2553
3134
|
class ToastContainerComponent {
|
|
2554
3135
|
toastService = inject(ToastService);
|
|
2555
|
-
|
|
2556
|
-
* Signal containing all active toast messages
|
|
2557
|
-
*/
|
|
3136
|
+
cdr = inject(ChangeDetectorRef);
|
|
2558
3137
|
toasts = this.toastService.toasts;
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
warning: 'bg-warning text-dark',
|
|
2566
|
-
info: 'bg-primary text-white'
|
|
2567
|
-
}), ...(ngDevMode ? [{ debugName: "toastClassMap" }] : []));
|
|
2568
|
-
/**
|
|
2569
|
-
* Computed signal that maps toast types to their Bootstrap icons
|
|
2570
|
-
*/
|
|
2571
|
-
toastIconMap = computed(() => ({
|
|
2572
|
-
success: 'bi-check-circle-fill',
|
|
2573
|
-
error: 'bi-exclamation-triangle-fill',
|
|
2574
|
-
warning: 'bi-exclamation-circle-fill',
|
|
2575
|
-
info: 'bi-info-circle-fill'
|
|
2576
|
-
}), ...(ngDevMode ? [{ debugName: "toastIconMap" }] : []));
|
|
2577
|
-
/**
|
|
2578
|
-
* Removes a toast message from the service
|
|
2579
|
-
*/
|
|
3138
|
+
constructor() {
|
|
3139
|
+
effect(() => {
|
|
3140
|
+
const currentToasts = this.toasts();
|
|
3141
|
+
this.cdr.detectChanges();
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2580
3144
|
remove(toast) {
|
|
2581
3145
|
this.toastService.remove(toast.id);
|
|
2582
3146
|
}
|
|
2583
|
-
/**
|
|
2584
|
-
* Gets the CSS class for a toast based on its type
|
|
2585
|
-
*/
|
|
2586
3147
|
getToastClass(type) {
|
|
2587
|
-
|
|
2588
|
-
|
|
3148
|
+
switch (type) {
|
|
3149
|
+
case 'success':
|
|
3150
|
+
return 'bg-success text-white';
|
|
3151
|
+
case 'error':
|
|
3152
|
+
return 'bg-danger text-white';
|
|
3153
|
+
case 'warning':
|
|
3154
|
+
return 'bg-warning text-dark';
|
|
3155
|
+
case 'info':
|
|
3156
|
+
default:
|
|
3157
|
+
return 'bg-primary text-white';
|
|
3158
|
+
}
|
|
2589
3159
|
}
|
|
2590
|
-
/**
|
|
2591
|
-
* Gets the Bootstrap icon class for a toast based on its type
|
|
2592
|
-
*/
|
|
2593
3160
|
getToastIcon(type) {
|
|
2594
|
-
|
|
2595
|
-
|
|
3161
|
+
switch (type) {
|
|
3162
|
+
case 'success':
|
|
3163
|
+
return 'bi-check-circle-fill';
|
|
3164
|
+
case 'error':
|
|
3165
|
+
return 'bi-exclamation-triangle-fill';
|
|
3166
|
+
case 'warning':
|
|
3167
|
+
return 'bi-exclamation-circle-fill';
|
|
3168
|
+
case 'info':
|
|
3169
|
+
default:
|
|
3170
|
+
return 'bi-info-circle-fill';
|
|
3171
|
+
}
|
|
2596
3172
|
}
|
|
2597
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
2598
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.
|
|
3173
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3174
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: ToastContainerComponent, isStandalone: true, selector: "ccc-toast-container", ngImport: i0, template: "<div class=\"toast-container position-fixed bottom-0 end-0 p-3\" style=\"z-index: 9999;\">\n @for (toast of toasts(); track toast.id) {\n <ngb-toast \n [class]=\"getToastClass(toast.type)\"\n [autohide]=\"true\"\n [delay]=\"toast.duration || 5000\"\n (hidden)=\"remove(toast)\">\n <ng-template ngbToastHeader>\n <i [class]=\"'bi ' + getToastIcon(toast.type) + ' me-2'\"></i>\n <strong class=\"me-auto\">\n {{ toast.type | titlecase }}\n </strong>\n </ng-template>\n {{ toast.message }}\n </ngb-toast>\n }\n</div>\n", styles: [".toast-container{max-width:400px}.toast-container .toast{margin-bottom:.5rem;border:none;box-shadow:0 .5rem 1rem #00000026}.toast-container .toast .toast-header{border-bottom:1px solid rgba(255,255,255,.1)}.toast-container .toast .toast-header .btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast-container .toast .toast-body{font-size:.875rem}[data-bs-theme=dark] .toast-container .toast.bg-warning{color:var(--bs-dark)!important}[data-bs-theme=dark] .toast-container .toast.bg-warning .toast-header{border-bottom-color:#0000001a}[data-bs-theme=dark] .toast-container .toast.bg-warning .toast-header .btn-close{filter:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: NgbModule }, { kind: "component", type: i2$1.NgbToast, selector: "ngb-toast", inputs: ["animation", "delay", "autohide", "header"], outputs: ["shown", "hidden"], exportAs: ["ngbToast"] }, { kind: "directive", type: i2$1.NgbToastHeader, selector: "[ngbToastHeader]" }, { kind: "pipe", type: i2.TitleCasePipe, name: "titlecase" }] });
|
|
3175
|
+
}
|
|
3176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
3177
|
+
type: Component,
|
|
3178
|
+
args: [{ selector: 'ccc-toast-container', standalone: true, imports: [CommonModule, NgbModule], template: "<div class=\"toast-container position-fixed bottom-0 end-0 p-3\" style=\"z-index: 9999;\">\n @for (toast of toasts(); track toast.id) {\n <ngb-toast \n [class]=\"getToastClass(toast.type)\"\n [autohide]=\"true\"\n [delay]=\"toast.duration || 5000\"\n (hidden)=\"remove(toast)\">\n <ng-template ngbToastHeader>\n <i [class]=\"'bi ' + getToastIcon(toast.type) + ' me-2'\"></i>\n <strong class=\"me-auto\">\n {{ toast.type | titlecase }}\n </strong>\n </ng-template>\n {{ toast.message }}\n </ngb-toast>\n }\n</div>\n", styles: [".toast-container{max-width:400px}.toast-container .toast{margin-bottom:.5rem;border:none;box-shadow:0 .5rem 1rem #00000026}.toast-container .toast .toast-header{border-bottom:1px solid rgba(255,255,255,.1)}.toast-container .toast .toast-header .btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast-container .toast .toast-body{font-size:.875rem}[data-bs-theme=dark] .toast-container .toast.bg-warning{color:var(--bs-dark)!important}[data-bs-theme=dark] .toast-container .toast.bg-warning .toast-header{border-bottom-color:#0000001a}[data-bs-theme=dark] .toast-container .toast.bg-warning .toast-header .btn-close{filter:none}\n"] }]
|
|
3179
|
+
}], ctorParameters: () => [] });
|
|
3180
|
+
|
|
3181
|
+
class PoweredByFooterComponent {
|
|
3182
|
+
siteConfigService = inject(SiteConfigService);
|
|
3183
|
+
siteConfig$ = this.siteConfigService.config$;
|
|
3184
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: PoweredByFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3185
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: PoweredByFooterComponent, isStandalone: true, selector: "ccc-powered-by-footer", ngImport: i0, template: "@if ((siteConfig$ | async)?.showPoweredBy !== false) {\r\n <div class=\"powered-by-footer\">\r\n <div class=\"footer-trigger\"></div>\r\n <div class=\"footer-content\">\r\n <span class=\"footer-text\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Powered by <strong>CUPCAKE Vanilla</strong>\r\n </span>\r\n </div>\r\n </div>\r\n}", styles: [".powered-by-footer{position:fixed;bottom:0;right:20px;z-index:1000;pointer-events:none}.footer-trigger{position:absolute;bottom:0;right:0;width:60px;height:20px;pointer-events:all}.footer-content{position:absolute;bottom:5px;right:0;background:#000c;backdrop-filter:blur(8px);color:#fff;padding:6px 12px;border-radius:20px;font-size:.75rem;font-weight:500;white-space:nowrap;opacity:0;transform:translateY(10px) scale(.95);transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;box-shadow:0 4px 12px #00000026}.dark-mode .footer-content,:root[data-bs-theme=dark] .footer-content{background:#ffffffe6;color:var(--cupcake-text)}.footer-trigger:hover+.footer-content,.footer-content:hover{opacity:1;transform:translateY(0) scale(1);pointer-events:all}.footer-text{display:flex;align-items:center;color:currentColor}.footer-text strong{background:linear-gradient(135deg,var(--cupcake-primary),var(--cupcake-primary-dark));background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:600}.dark-mode .footer-text strong,:root[data-bs-theme=dark] .footer-text strong{-webkit-text-fill-color:var(--cupcake-primary);background:none;color:var(--cupcake-primary)}.footer-text i{color:#ffc107;font-size:.8rem}.dark-mode .footer-text i,:root[data-bs-theme=dark] .footer-text i{color:#ffeb3b}.powered-by-footer{animation:slideInFromBottom .5s ease-out 2s both}@keyframes slideInFromBottom{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@media (max-width: 768px){.powered-by-footer{display:none}}.powered-by-footer{user-select:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] });
|
|
2599
3186
|
}
|
|
2600
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
3187
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: PoweredByFooterComponent, decorators: [{
|
|
2601
3188
|
type: Component,
|
|
2602
|
-
args: [{ selector: '
|
|
3189
|
+
args: [{ selector: 'ccc-powered-by-footer', standalone: true, imports: [CommonModule], template: "@if ((siteConfig$ | async)?.showPoweredBy !== false) {\r\n <div class=\"powered-by-footer\">\r\n <div class=\"footer-trigger\"></div>\r\n <div class=\"footer-content\">\r\n <span class=\"footer-text\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Powered by <strong>CUPCAKE Vanilla</strong>\r\n </span>\r\n </div>\r\n </div>\r\n}", styles: [".powered-by-footer{position:fixed;bottom:0;right:20px;z-index:1000;pointer-events:none}.footer-trigger{position:absolute;bottom:0;right:0;width:60px;height:20px;pointer-events:all}.footer-content{position:absolute;bottom:5px;right:0;background:#000c;backdrop-filter:blur(8px);color:#fff;padding:6px 12px;border-radius:20px;font-size:.75rem;font-weight:500;white-space:nowrap;opacity:0;transform:translateY(10px) scale(.95);transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;box-shadow:0 4px 12px #00000026}.dark-mode .footer-content,:root[data-bs-theme=dark] .footer-content{background:#ffffffe6;color:var(--cupcake-text)}.footer-trigger:hover+.footer-content,.footer-content:hover{opacity:1;transform:translateY(0) scale(1);pointer-events:all}.footer-text{display:flex;align-items:center;color:currentColor}.footer-text strong{background:linear-gradient(135deg,var(--cupcake-primary),var(--cupcake-primary-dark));background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:600}.dark-mode .footer-text strong,:root[data-bs-theme=dark] .footer-text strong{-webkit-text-fill-color:var(--cupcake-primary);background:none;color:var(--cupcake-primary)}.footer-text i{color:#ffc107;font-size:.8rem}.dark-mode .footer-text i,:root[data-bs-theme=dark] .footer-text i{color:#ffeb3b}.powered-by-footer{animation:slideInFromBottom .5s ease-out 2s both}@keyframes slideInFromBottom{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@media (max-width: 768px){.powered-by-footer{display:none}}.powered-by-footer{user-select:none}\n"] }]
|
|
2603
3190
|
}] });
|
|
2604
3191
|
|
|
2605
3192
|
// Auth components
|
|
@@ -2614,37 +3201,32 @@ class CupcakeCoreModule {
|
|
|
2614
3201
|
]
|
|
2615
3202
|
};
|
|
2616
3203
|
}
|
|
2617
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
2618
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.
|
|
3204
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: CupcakeCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
3205
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.0", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
|
|
2619
3206
|
ReactiveFormsModule,
|
|
2620
3207
|
HttpClientModule,
|
|
2621
3208
|
RouterModule,
|
|
2622
3209
|
NgbModule,
|
|
2623
|
-
// Standalone components
|
|
2624
3210
|
LoginComponent,
|
|
2625
3211
|
RegisterComponent,
|
|
2626
3212
|
ToastContainerComponent], exports: [LoginComponent,
|
|
2627
3213
|
RegisterComponent,
|
|
2628
3214
|
ToastContainerComponent,
|
|
2629
|
-
// Re-export modules that consumers might need
|
|
2630
3215
|
CommonModule,
|
|
2631
3216
|
ReactiveFormsModule,
|
|
2632
3217
|
NgbModule] });
|
|
2633
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.
|
|
3218
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
|
|
2634
3219
|
ReactiveFormsModule,
|
|
2635
3220
|
HttpClientModule,
|
|
2636
3221
|
RouterModule,
|
|
2637
3222
|
NgbModule,
|
|
2638
|
-
// Standalone components
|
|
2639
3223
|
LoginComponent,
|
|
2640
3224
|
RegisterComponent,
|
|
2641
|
-
ToastContainerComponent,
|
|
2642
|
-
// Re-export modules that consumers might need
|
|
2643
|
-
CommonModule,
|
|
3225
|
+
ToastContainerComponent, CommonModule,
|
|
2644
3226
|
ReactiveFormsModule,
|
|
2645
3227
|
NgbModule] });
|
|
2646
3228
|
}
|
|
2647
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
3229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: CupcakeCoreModule, decorators: [{
|
|
2648
3230
|
type: NgModule,
|
|
2649
3231
|
args: [{
|
|
2650
3232
|
declarations: [],
|
|
@@ -2654,7 +3236,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
2654
3236
|
HttpClientModule,
|
|
2655
3237
|
RouterModule,
|
|
2656
3238
|
NgbModule,
|
|
2657
|
-
// Standalone components
|
|
2658
3239
|
LoginComponent,
|
|
2659
3240
|
RegisterComponent,
|
|
2660
3241
|
ToastContainerComponent
|
|
@@ -2663,7 +3244,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
2663
3244
|
LoginComponent,
|
|
2664
3245
|
RegisterComponent,
|
|
2665
3246
|
ToastContainerComponent,
|
|
2666
|
-
// Re-export modules that consumers might need
|
|
2667
3247
|
CommonModule,
|
|
2668
3248
|
ReactiveFormsModule,
|
|
2669
3249
|
NgbModule
|
|
@@ -2674,11 +3254,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
2674
3254
|
/*
|
|
2675
3255
|
* Public API Surface of cupcake-core
|
|
2676
3256
|
*/
|
|
2677
|
-
// Models
|
|
2678
3257
|
|
|
2679
3258
|
/**
|
|
2680
3259
|
* Generated bundle index. Do not edit.
|
|
2681
3260
|
*/
|
|
2682
3261
|
|
|
2683
|
-
export { ApiService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, adminGuard, authGuard, authInterceptor };
|
|
3262
|
+
export { ApiService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor };
|
|
2684
3263
|
//# sourceMappingURL=noatgnu-cupcake-core.mjs.map
|