@noatgnu/cupcake-core 1.0.0 → 1.1.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.
@@ -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.1.7", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
338
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: AuthService, providedIn: 'root' });
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.1.7", ngImport: i0, type: AuthService, decorators: [{
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.is_public !== undefined) {
453
- transformed.visibility = this.convertLegacyVisibility(legacyData.is_public, legacyData.is_default);
454
- delete transformed.is_public;
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.is_public = this.convertToLegacyVisibility(prepared.visibility);
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.1.7", ngImport: i0, type: ResourceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
484
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ResourceService, providedIn: 'root' });
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.1.7", ngImport: i0, type: ResourceService, decorators: [{
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.1.7", ngImport: i0, type: ApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
828
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ApiService, providedIn: 'root' });
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.1.7", ngImport: i0, type: ApiService, decorators: [{
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.1.7", ngImport: i0, type: BaseApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
933
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: BaseApiService, providedIn: 'root' });
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.1.7", ngImport: i0, type: BaseApiService, decorators: [{
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.1.7", ngImport: i0, type: SiteConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1009
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
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.1.7", ngImport: i0, type: SiteConfigService, decorators: [{
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 ToastService {
1019
- toastsSignal = signal([], ...(ngDevMode ? [{ debugName: "toastsSignal" }] : []));
1020
- // Public readonly signal for components to subscribe to
1021
- toasts = this.toastsSignal.asReadonly();
1022
- show(message, type = 'info', duration = 5000) {
1023
- const id = this.generateId();
1024
- const toast = {
1025
- id,
1026
- message,
1027
- type,
1028
- duration,
1029
- dismissible: true
1030
- };
1031
- this.toastsSignal.update(toasts => [...toasts, toast]);
1032
- // Auto-remove after duration
1033
- if (duration > 0) {
1034
- setTimeout(() => {
1035
- this.remove(id);
1036
- }, duration);
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
- success(message, duration = 5000) {
1040
- this.show(message, 'success', duration);
1139
+ setTheme(theme) {
1140
+ this._theme.set(theme);
1141
+ localStorage.setItem(this.THEME_KEY, theme);
1142
+ this.updateDocumentTheme();
1041
1143
  }
1042
- error(message, duration = 8000) {
1043
- this.show(message, 'error', duration);
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
- warning(message, duration = 6000) {
1046
- this.show(message, 'warning', duration);
1156
+ loadStoredTheme() {
1157
+ const stored = localStorage.getItem(this.THEME_KEY);
1158
+ return stored && ['light', 'dark', 'auto'].includes(stored) ? stored : 'auto';
1047
1159
  }
1048
- info(message, duration = 5000) {
1049
- this.show(message, 'info', duration);
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
- remove(id) {
1052
- this.toastsSignal.update(toasts => toasts.filter(toast => toast.id !== id));
1053
- }
1054
- clear() {
1055
- this.toastsSignal.set([]);
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
- generateId() {
1058
- return Math.random().toString(36).substring(2) + Date.now().toString(36);
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.1.7", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1061
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: ToastService, providedIn: 'root' });
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.1.7", ngImport: i0, type: ToastService, decorators: [{
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.1.7", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1138
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: UserManagementService, providedIn: 'root' });
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.1.7", ngImport: i0, type: UserManagementService, decorators: [{
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.1.7", ngImport: i0, type: LabGroupService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1196
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: LabGroupService, providedIn: 'root' });
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.1.7", ngImport: i0, type: LabGroupService, decorators: [{
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: state.url }
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
- router.navigate(['/login']);
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
- // If refresh is in progress, wait for it to complete
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.1.7", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1497
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: LoginComponent, isStandalone: true, selector: "app-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" }] });
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.1.7", ngImport: i0, type: LoginComponent, decorators: [{
2086
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: LoginComponent, decorators: [{
1500
2087
  type: Component,
1501
- args: [{ selector: 'app-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"] }]
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.1.7", ngImport: i0, type: RegisterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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.1.7", ngImport: i0, type: RegisterComponent, decorators: [{
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.1.7", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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.1.7", ngImport: i0, type: UserManagementComponent, decorators: [{
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.1.7", ngImport: i0, type: LabGroupsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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.1.7", ngImport: i0, type: LabGroupsComponent, decorators: [{
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.1.7", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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.1.7", ngImport: i0, type: UserProfileComponent, decorators: [{
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.1.7", ngImport: i0, type: SiteConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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.1.7", ngImport: i0, type: SiteConfigComponent, decorators: [{
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
- * Computed signal that maps toast types to their CSS classes
2561
- */
2562
- toastClassMap = computed(() => ({
2563
- success: 'bg-success text-white',
2564
- error: 'bg-danger text-white',
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
- const classMap = this.toastClassMap();
2588
- return classMap[type] || classMap.info;
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
- const iconMap = this.toastIconMap();
2595
- return iconMap[type] || iconMap.info;
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.1.7", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2598
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: ToastContainerComponent, isStandalone: true, selector: "app-toast-container", ngImport: i0, template: "<div class=\"toast-container position-fixed top-0 end-0 p-3\" style=\"z-index: 1055;\">\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'\" \n [attr.aria-hidden]=\"true\"></i>\n <strong class=\"me-auto\">\n {{ toast.type | titlecase }}\n </strong>\n </ng-template>\n {{ toast.message }}\n </ngb-toast>\n } @empty {\n <!-- No toasts to display -->\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" }] });
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.1.7", ngImport: i0, type: ToastContainerComponent, decorators: [{
3187
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: PoweredByFooterComponent, decorators: [{
2601
3188
  type: Component,
2602
- args: [{ selector: 'app-toast-container', standalone: true, imports: [CommonModule, NgbModule], template: "<div class=\"toast-container position-fixed top-0 end-0 p-3\" style=\"z-index: 1055;\">\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'\" \n [attr.aria-hidden]=\"true\"></i>\n <strong class=\"me-auto\">\n {{ toast.type | titlecase }}\n </strong>\n </ng-template>\n {{ toast.message }}\n </ngb-toast>\n } @empty {\n <!-- No toasts to display -->\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"] }]
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.1.7", ngImport: i0, type: CupcakeCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2618
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.1.7", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
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.1.7", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
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.1.7", ngImport: i0, type: CupcakeCoreModule, decorators: [{
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