@noatgnu/cupcake-core 1.3.14 → 1.3.16

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.
@@ -165,7 +165,10 @@ const authInterceptor = (req, next) => {
165
165
  req.url.includes('/auth/token/') ||
166
166
  req.url.includes('/auth/orcid/') ||
167
167
  req.url.includes('/auth/register/') ||
168
- req.url.includes('/site-config/public/')) {
168
+ req.url.includes('/auth/exchange-code/') ||
169
+ req.url.includes('/site-config/public/') ||
170
+ req.url.includes('/users/auth_config/') ||
171
+ req.url.includes('/users/registration_status/')) {
169
172
  return next(req);
170
173
  }
171
174
  const token = localStorage.getItem('ccvAccessToken');
@@ -291,10 +294,10 @@ class DemoModeService {
291
294
  this.setDemoMode(true, cleanupInterval);
292
295
  }
293
296
  }
294
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
295
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeService, providedIn: 'root' });
297
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
298
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeService, providedIn: 'root' });
296
299
  }
297
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeService, decorators: [{
300
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeService, decorators: [{
298
301
  type: Injectable,
299
302
  args: [{
300
303
  providedIn: 'root'
@@ -320,10 +323,10 @@ class DemoModeInterceptor {
320
323
  }
321
324
  }));
322
325
  }
323
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeInterceptor, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
324
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeInterceptor });
326
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeInterceptor, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
327
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeInterceptor });
325
328
  }
326
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeInterceptor, decorators: [{
329
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeInterceptor, decorators: [{
327
330
  type: Injectable
328
331
  }], ctorParameters: () => [{ type: DemoModeService }] });
329
332
 
@@ -441,13 +444,19 @@ class AuthService {
441
444
  return null;
442
445
  }
443
446
  }
444
- initiateORCIDLogin() {
445
- return this.http.get(`${this.apiUrl}/auth/orcid/login/`)
447
+ initiateORCIDLogin(rememberMe = false) {
448
+ const params = rememberMe ? '?remember_me=true' : '';
449
+ return this.http.get(`${this.apiUrl}/auth/orcid/login/${params}`)
446
450
  .pipe(map(response => ({
447
451
  authorizationUrl: response.authorization_url,
448
452
  state: response.state
449
453
  })));
450
454
  }
455
+ exchangeAuthCode(authCode) {
456
+ return this.http.post(`${this.apiUrl}/auth/exchange-code/`, {
457
+ auth_code: authCode
458
+ }).pipe(tap$1(response => this.setAuthData(response)));
459
+ }
451
460
  handleORCIDCallback(code, state, rememberMe = false) {
452
461
  const params = new URLSearchParams({ code, state, remember_me: rememberMe.toString() });
453
462
  return this.http.get(`${this.apiUrl}/auth/orcid/callback/?${params}`)
@@ -504,6 +513,9 @@ class AuthService {
504
513
  getRefreshToken() {
505
514
  return localStorage.getItem('ccvRefreshToken');
506
515
  }
516
+ handleExternalLogin(response) {
517
+ this.setAuthData(response);
518
+ }
507
519
  setAuthData(response) {
508
520
  const accessToken = response.accessToken || response.access_token || response.access;
509
521
  const refreshToken = response.refreshToken || response.refresh_token || response.refresh;
@@ -580,10 +592,10 @@ class AuthService {
580
592
  this.currentUserSubject.next(null);
581
593
  this.isAuthenticatedSubject.next(false);
582
594
  }
583
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
584
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AuthService, providedIn: 'root' });
595
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
596
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, providedIn: 'root' });
585
597
  }
586
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AuthService, decorators: [{
598
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AuthService, decorators: [{
587
599
  type: Injectable,
588
600
  args: [{
589
601
  providedIn: 'root'
@@ -726,10 +738,10 @@ class ResourceService {
726
738
  }
727
739
  return prepared;
728
740
  }
729
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ResourceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
730
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ResourceService, providedIn: 'root' });
741
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ResourceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
742
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ResourceService, providedIn: 'root' });
731
743
  }
732
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ResourceService, decorators: [{
744
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ResourceService, decorators: [{
733
745
  type: Injectable,
734
746
  args: [{
735
747
  providedIn: 'root'
@@ -816,7 +828,7 @@ class ApiService {
816
828
  }
817
829
  // USER PROFILE
818
830
  getUserProfile() {
819
- return this.http.get(`${this.apiUrl}/auth/profile/`);
831
+ return this.get(`${this.apiUrl}/auth/profile/`);
820
832
  }
821
833
  // ===================================================================
822
834
  // SITE CONFIGURATION METHODS
@@ -843,86 +855,79 @@ class ApiService {
843
855
  httpParams = httpParams.set('limit', params.limit.toString());
844
856
  if (params?.offset !== undefined)
845
857
  httpParams = httpParams.set('offset', params.offset.toString());
846
- return this.http.get(`${this.apiUrl}/users/`, { params: httpParams });
858
+ return this.get(`${this.apiUrl}/users/`, { params: httpParams });
847
859
  }
848
860
  getUser(id) {
849
- return this.http.get(`${this.apiUrl}/users/${id}/`);
861
+ return this.get(`${this.apiUrl}/users/${id}/`);
850
862
  }
851
863
  createUser(userData) {
852
864
  return this.post(`${this.apiUrl}/users/admin_create/`, userData);
853
865
  }
854
866
  updateUser(id, userData) {
855
- return this.http.patch(`${this.apiUrl}/users/${id}/`, userData);
867
+ return this.patch(`${this.apiUrl}/users/${id}/`, userData);
856
868
  }
857
869
  deleteUser(id) {
858
- return this.http.delete(`${this.apiUrl}/users/${id}/`);
870
+ return this.delete(`${this.apiUrl}/users/${id}/`);
859
871
  }
860
872
  // Public user registration
861
873
  registerUser(userData) {
862
- return this.http.post(`${this.apiUrl}/users/register/`, userData);
874
+ return this.post(`${this.apiUrl}/users/register/`, userData);
863
875
  }
864
876
  // Authentication configuration
865
877
  getAuthConfig() {
866
- return this.http.get(`${this.apiUrl}/users/auth_config/`);
878
+ return this.get(`${this.apiUrl}/users/auth_config/`);
867
879
  }
868
880
  getRegistrationStatus() {
869
- return this.http.get(`${this.apiUrl}/users/registration_status/`);
881
+ return this.get(`${this.apiUrl}/users/registration_status/`);
870
882
  }
871
883
  // ===================================================================
872
884
  // PASSWORD MANAGEMENT METHODS
873
885
  // ===================================================================
874
886
  // User password change (authenticated user)
875
887
  changePassword(passwordData) {
876
- return this.http.post(`${this.apiUrl}/users/change_password/`, passwordData);
888
+ return this.post(`${this.apiUrl}/users/change_password/`, passwordData);
877
889
  }
878
890
  // User profile update
879
891
  updateProfile(profileData) {
880
- return this.http.post(`${this.apiUrl}/users/update_profile/`, profileData);
892
+ return this.post(`${this.apiUrl}/users/update_profile/`, profileData);
881
893
  }
882
894
  // Email change with verification
883
895
  requestEmailChange(emailData) {
884
- return this.http.post(`${this.apiUrl}/users/request_email_change/`, emailData);
896
+ return this.post(`${this.apiUrl}/users/request_email_change/`, emailData);
885
897
  }
886
898
  confirmEmailChange(confirmData) {
887
- return this.http.post(`${this.apiUrl}/users/confirm_email_change/`, confirmData);
899
+ return this.post(`${this.apiUrl}/users/confirm_email_change/`, confirmData);
888
900
  }
889
901
  // Admin password reset
890
902
  resetUserPassword(userId, passwordData) {
891
- const apiData = {
892
- user_id: passwordData.userId,
893
- new_password: passwordData.newPassword,
894
- confirm_password: passwordData.confirmPassword,
895
- force_password_change: passwordData.forcePasswordChange,
896
- reason: passwordData.reason
897
- };
898
- return this.http.post(`${this.apiUrl}/users/${userId}/reset_password/`, apiData);
903
+ return this.post(`${this.apiUrl}/users/${userId}/reset_password/`, passwordData);
899
904
  }
900
905
  // Password reset request (forgot password)
901
906
  requestPasswordReset(resetData) {
902
- return this.http.post(`${this.apiUrl}/users/request_password_reset/`, resetData);
907
+ return this.post(`${this.apiUrl}/users/request_password_reset/`, resetData);
903
908
  }
904
909
  // Confirm password reset with token
905
910
  confirmPasswordReset(confirmData) {
906
- return this.http.post(`${this.apiUrl}/users/confirm_password_reset/`, confirmData);
911
+ return this.post(`${this.apiUrl}/users/confirm_password_reset/`, confirmData);
907
912
  }
908
913
  // ===================================================================
909
914
  // ACCOUNT LINKING METHODS
910
915
  // ===================================================================
911
916
  // Link ORCID to current user account
912
917
  linkOrcid(orcidData) {
913
- return this.http.post(`${this.apiUrl}/users/link_orcid/`, orcidData);
918
+ return this.post(`${this.apiUrl}/users/link_orcid/`, orcidData);
914
919
  }
915
920
  // Unlink ORCID from current user account
916
921
  unlinkOrcid() {
917
- return this.http.delete(`${this.apiUrl}/users/unlink_orcid/`);
922
+ return this.delete(`${this.apiUrl}/users/unlink_orcid/`);
918
923
  }
919
924
  // Detect duplicate accounts
920
925
  detectDuplicateAccounts(searchData) {
921
- return this.http.post(`${this.apiUrl}/users/detect_duplicates/`, searchData);
926
+ return this.post(`${this.apiUrl}/users/detect_duplicates/`, searchData);
922
927
  }
923
928
  // Request account merge
924
929
  requestAccountMerge(mergeData) {
925
- return this.http.post(`${this.apiUrl}/users/request_merge/`, mergeData);
930
+ return this.post(`${this.apiUrl}/users/request_merge/`, mergeData);
926
931
  }
927
932
  // ANNOTATION MANAGEMENT
928
933
  getAnnotationFolders(params) {
@@ -1124,10 +1129,10 @@ class ApiService {
1124
1129
  testRemoteHostConnection(id) {
1125
1130
  return this.http.post(`${this.apiUrl}/remote-hosts/${id}/test_connection/`, {});
1126
1131
  }
1127
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
1128
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ApiService, providedIn: 'root' });
1132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
1133
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiService, providedIn: 'root' });
1129
1134
  }
1130
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ApiService, decorators: [{
1135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ApiService, decorators: [{
1131
1136
  type: Injectable,
1132
1137
  args: [{
1133
1138
  providedIn: 'root'
@@ -1230,10 +1235,10 @@ class BaseApiService {
1230
1235
  }
1231
1236
  return httpParams;
1232
1237
  }
1233
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: BaseApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1234
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: BaseApiService, providedIn: 'root' });
1238
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: BaseApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1239
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: BaseApiService, providedIn: 'root' });
1235
1240
  }
1236
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: BaseApiService, decorators: [{
1241
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: BaseApiService, decorators: [{
1237
1242
  type: Injectable,
1238
1243
  args: [{
1239
1244
  providedIn: 'root'
@@ -1536,10 +1541,10 @@ class WebSocketService {
1536
1541
  }
1537
1542
  });
1538
1543
  }
1539
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
1540
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketService, providedIn: 'root' });
1544
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
1545
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketService, providedIn: 'root' });
1541
1546
  }
1542
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketService, decorators: [{
1547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketService, decorators: [{
1543
1548
  type: Injectable,
1544
1549
  args: [{
1545
1550
  providedIn: 'root'
@@ -1662,10 +1667,10 @@ class AsyncTaskMonitorService extends BaseApiService {
1662
1667
  }
1663
1668
  });
1664
1669
  }
1665
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AsyncTaskMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1666
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AsyncTaskMonitorService, providedIn: 'root' });
1670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AsyncTaskMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1671
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AsyncTaskMonitorService, providedIn: 'root' });
1667
1672
  }
1668
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AsyncTaskMonitorService, decorators: [{
1673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AsyncTaskMonitorService, decorators: [{
1669
1674
  type: Injectable,
1670
1675
  args: [{
1671
1676
  providedIn: 'root'
@@ -1712,10 +1717,10 @@ class ToastService {
1712
1717
  generateId() {
1713
1718
  return Math.random().toString(36).substring(2) + Date.now().toString(36);
1714
1719
  }
1715
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1716
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ToastService, providedIn: 'root' });
1720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1721
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ToastService, providedIn: 'root' });
1717
1722
  }
1718
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ToastService, decorators: [{
1723
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ToastService, decorators: [{
1719
1724
  type: Injectable,
1720
1725
  args: [{
1721
1726
  providedIn: 'root'
@@ -1785,10 +1790,10 @@ class NotificationService {
1785
1790
  this.toastService.info(`${title}: ${message}`);
1786
1791
  }
1787
1792
  }
1788
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NotificationService, deps: [{ token: ToastService }], target: i0.ɵɵFactoryTarget.Injectable });
1789
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NotificationService, providedIn: 'root' });
1793
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: NotificationService, deps: [{ token: ToastService }], target: i0.ɵɵFactoryTarget.Injectable });
1794
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: NotificationService, providedIn: 'root' });
1790
1795
  }
1791
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: NotificationService, decorators: [{
1796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: NotificationService, decorators: [{
1792
1797
  type: Injectable,
1793
1798
  args: [{
1794
1799
  providedIn: 'root'
@@ -1953,10 +1958,10 @@ class SiteConfigService extends BaseApiService {
1953
1958
  }
1954
1959
  return { valid: true };
1955
1960
  }
1956
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: SiteConfigService, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
1957
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
1961
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SiteConfigService, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
1962
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
1958
1963
  }
1959
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: SiteConfigService, decorators: [{
1964
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SiteConfigService, decorators: [{
1960
1965
  type: Injectable,
1961
1966
  args: [{
1962
1967
  providedIn: 'root'
@@ -2027,10 +2032,10 @@ class ThemeService {
2027
2032
  default: return 'Auto Mode';
2028
2033
  }
2029
2034
  }
2030
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2031
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ThemeService, providedIn: 'root' });
2035
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2036
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ThemeService, providedIn: 'root' });
2032
2037
  }
2033
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ThemeService, decorators: [{
2038
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ThemeService, decorators: [{
2034
2039
  type: Injectable,
2035
2040
  args: [{
2036
2041
  providedIn: 'root'
@@ -2081,6 +2086,9 @@ class UserManagementService {
2081
2086
  if (user.firstName || user.lastName) {
2082
2087
  return `${user.firstName} ${user.lastName}`.trim();
2083
2088
  }
2089
+ if (user.orcidName) {
2090
+ return user.orcidName;
2091
+ }
2084
2092
  return user.username || user.email || 'User';
2085
2093
  }
2086
2094
  formatDate(dateString) {
@@ -2104,10 +2112,10 @@ class UserManagementService {
2104
2112
  getCurrentTotalUsers() {
2105
2113
  return this.totalUsersSubject.getValue();
2106
2114
  }
2107
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2108
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserManagementService, providedIn: 'root' });
2115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserManagementService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2116
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserManagementService, providedIn: 'root' });
2109
2117
  }
2110
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserManagementService, decorators: [{
2118
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserManagementService, decorators: [{
2111
2119
  type: Injectable,
2112
2120
  args: [{
2113
2121
  providedIn: 'root'
@@ -2159,10 +2167,10 @@ class WebSocketConfigService {
2159
2167
  const notificationEndpoint = endpoints.find(e => e.endpoint.includes('notifications'));
2160
2168
  return notificationEndpoint?.endpoint || endpoints[0].endpoint;
2161
2169
  }
2162
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2163
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketConfigService, providedIn: 'root' });
2170
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2171
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketConfigService, providedIn: 'root' });
2164
2172
  }
2165
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: WebSocketConfigService, decorators: [{
2173
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: WebSocketConfigService, decorators: [{
2166
2174
  type: Injectable,
2167
2175
  args: [{
2168
2176
  providedIn: 'root'
@@ -2182,10 +2190,10 @@ class AdminWebSocketService extends WebSocketService {
2182
2190
  getSystemNotifications() {
2183
2191
  return this.filterMessages('system.notification');
2184
2192
  }
2185
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AdminWebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
2186
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AdminWebSocketService, providedIn: 'root' });
2193
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AdminWebSocketService, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Injectable });
2194
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AdminWebSocketService, providedIn: 'root' });
2187
2195
  }
2188
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: AdminWebSocketService, decorators: [{
2196
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: AdminWebSocketService, decorators: [{
2189
2197
  type: Injectable,
2190
2198
  args: [{
2191
2199
  providedIn: 'root'
@@ -2287,10 +2295,10 @@ class LabGroupService extends BaseApiService {
2287
2295
  getLabGroupPermissionsForUser(userId) {
2288
2296
  return this.getLabGroupPermissions({ user: userId });
2289
2297
  }
2290
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LabGroupService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2291
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LabGroupService, providedIn: 'root' });
2298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LabGroupService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2299
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LabGroupService, providedIn: 'root' });
2292
2300
  }
2293
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LabGroupService, decorators: [{
2301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LabGroupService, decorators: [{
2294
2302
  type: Injectable,
2295
2303
  args: [{
2296
2304
  providedIn: 'root'
@@ -2414,6 +2422,7 @@ class LoginComponent {
2414
2422
  returnUrl = '/';
2415
2423
  ngOnInit() {
2416
2424
  this.loadAuthConfig();
2425
+ this.handleHashParams();
2417
2426
  this.route.queryParams.subscribe(params => {
2418
2427
  this.returnUrl = this.cleanReturnUrl(params['returnUrl']) || '/';
2419
2428
  if (params['code'] && params['state']) {
@@ -2435,6 +2444,46 @@ class LoginComponent {
2435
2444
  }
2436
2445
  });
2437
2446
  }
2447
+ handleHashParams() {
2448
+ const hash = window.location.hash;
2449
+ const queryIndex = hash.indexOf('?');
2450
+ if (queryIndex === -1)
2451
+ return;
2452
+ const queryString = hash.substring(queryIndex + 1);
2453
+ const params = new URLSearchParams(queryString);
2454
+ const authCode = params.get('auth_code');
2455
+ const errorParam = params.get('error');
2456
+ if (errorParam) {
2457
+ this.error.set(`ORCID authentication failed: ${errorParam}`);
2458
+ this.cleanHashParams();
2459
+ return;
2460
+ }
2461
+ if (authCode) {
2462
+ this.loading.set(true);
2463
+ this.error.set(null);
2464
+ this.cleanHashParams();
2465
+ this.authService.exchangeAuthCode(authCode).subscribe({
2466
+ next: () => {
2467
+ this.success.set('Login successful!');
2468
+ setTimeout(() => {
2469
+ this.navigateToReturnUrl();
2470
+ }, 1000);
2471
+ },
2472
+ error: (error) => {
2473
+ this.loading.set(false);
2474
+ this.error.set(error.error?.error || error.error?.detail || 'Failed to complete authentication.');
2475
+ }
2476
+ });
2477
+ }
2478
+ }
2479
+ cleanHashParams() {
2480
+ const hash = window.location.hash;
2481
+ const queryIndex = hash.indexOf('?');
2482
+ if (queryIndex !== -1) {
2483
+ const cleanHash = hash.substring(0, queryIndex);
2484
+ window.history.replaceState(null, '', window.location.pathname + cleanHash);
2485
+ }
2486
+ }
2438
2487
  /**
2439
2488
  * Navigate to return URL, properly handling query parameters
2440
2489
  */
@@ -2540,7 +2589,8 @@ class LoginComponent {
2540
2589
  loginWithORCID() {
2541
2590
  this.loading.set(true);
2542
2591
  this.error.set(null);
2543
- this.authService.initiateORCIDLogin().subscribe({
2592
+ const rememberMe = this.loginForm.get('rememberMe')?.value || false;
2593
+ this.authService.initiateORCIDLogin(rememberMe).subscribe({
2544
2594
  next: (response) => {
2545
2595
  sessionStorage.setItem('orcid_state', response.state);
2546
2596
  window.location.href = response.authorizationUrl;
@@ -2593,8 +2643,14 @@ class LoginComponent {
2593
2643
  /**
2594
2644
  * Computed signals for UI display logic
2595
2645
  */
2596
- shouldShowOrcidLogin = computed(() => this.authConfig()?.orcidLoginEnabled === true, ...(ngDevMode ? [{ debugName: "shouldShowOrcidLogin" }] : []));
2597
- shouldShowRegistration = computed(() => this.registrationStatus()?.registrationEnabled === true, ...(ngDevMode ? [{ debugName: "shouldShowRegistration" }] : []));
2646
+ shouldShowOrcidLogin = computed(() => {
2647
+ const config = this.siteConfigService.configSubject.value;
2648
+ return config.enableOrcidLogin === true || this.authConfig()?.orcidLoginEnabled === true;
2649
+ }, ...(ngDevMode ? [{ debugName: "shouldShowOrcidLogin" }] : []));
2650
+ shouldShowRegistration = computed(() => {
2651
+ const config = this.siteConfigService.configSubject.value;
2652
+ return config.allowUserRegistration === true || this.registrationStatus()?.registrationEnabled === true;
2653
+ }, ...(ngDevMode ? [{ debugName: "shouldShowRegistration" }] : []));
2598
2654
  shouldShowRegularLogin = computed(() => this.authConfig()?.regularLoginEnabled !== false, ...(ngDevMode ? [{ debugName: "shouldShowRegularLogin" }] : []));
2599
2655
  registrationMessage = computed(() => this.registrationStatus()?.message || 'Registration is currently enabled', ...(ngDevMode ? [{ debugName: "registrationMessage" }] : []));
2600
2656
  rememberMeDuration = computed(() => {
@@ -2610,10 +2666,10 @@ class LoginComponent {
2610
2666
  queryParams: { returnUrl: this.returnUrl }
2611
2667
  });
2612
2668
  }
2613
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2614
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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 <div class=\"mb-3 form-check\">\r\n <input\r\n type=\"checkbox\"\r\n class=\"form-check-input\"\r\n id=\"rememberMe\"\r\n formControlName=\"rememberMe\">\r\n <label class=\"form-check-label\" for=\"rememberMe\">\r\n <i class=\"bi bi-clock-history me-1\"></i>\r\n Remember me for {{ rememberMeDuration() }} days\r\n </label>\r\n <div class=\"form-text\">\r\n Keep me logged in on this device\r\n </div>\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.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: "pipe", type: i2.AsyncPipe, name: "async" }] });
2669
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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 <div class=\"mb-3 form-check\">\r\n <input\r\n type=\"checkbox\"\r\n class=\"form-check-input\"\r\n id=\"rememberMe\"\r\n formControlName=\"rememberMe\">\r\n <label class=\"form-check-label\" for=\"rememberMe\">\r\n <i class=\"bi bi-clock-history me-1\"></i>\r\n Remember me for {{ rememberMeDuration() }} days\r\n </label>\r\n <div class=\"form-text\">\r\n Keep me logged in on this device\r\n </div>\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.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: "pipe", type: i2.AsyncPipe, name: "async" }] });
2615
2671
  }
2616
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LoginComponent, decorators: [{
2672
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LoginComponent, decorators: [{
2617
2673
  type: Component,
2618
2674
  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 <div class=\"mb-3 form-check\">\r\n <input\r\n type=\"checkbox\"\r\n class=\"form-check-input\"\r\n id=\"rememberMe\"\r\n formControlName=\"rememberMe\">\r\n <label class=\"form-check-label\" for=\"rememberMe\">\r\n <i class=\"bi bi-clock-history me-1\"></i>\r\n Remember me for {{ rememberMeDuration() }} days\r\n </label>\r\n <div class=\"form-text\">\r\n Keep me logged in on this device\r\n </div>\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"] }]
2619
2675
  }], ctorParameters: () => [] });
@@ -2804,10 +2860,10 @@ class RegisterComponent {
2804
2860
  canSubmitForm = computed(() => {
2805
2861
  return this.registrationForm.valid && this.registrationEnabled() && !this.loading();
2806
2862
  }, ...(ngDevMode ? [{ debugName: "canSubmitForm" }] : []));
2807
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: RegisterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2808
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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" }] });
2863
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: RegisterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2864
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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" }] });
2809
2865
  }
2810
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: RegisterComponent, decorators: [{
2866
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: RegisterComponent, decorators: [{
2811
2867
  type: Component,
2812
2868
  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"] }]
2813
2869
  }], ctorParameters: () => [] });
@@ -3024,10 +3080,10 @@ class UserManagementComponent {
3024
3080
  getUserDisplayName(user) {
3025
3081
  return this.userManagementService.getUserDisplayName(user);
3026
3082
  }
3027
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3028
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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 }] });
3083
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserManagementComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3084
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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 }] });
3029
3085
  }
3030
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserManagementComponent, decorators: [{
3086
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserManagementComponent, decorators: [{
3031
3087
  type: Component,
3032
3088
  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>" }]
3033
3089
  }], ctorParameters: () => [] });
@@ -3060,6 +3116,7 @@ class LabGroupsComponent {
3060
3116
  selectedGroup = signal(null, ...(ngDevMode ? [{ debugName: "selectedGroup" }] : []));
3061
3117
  groupMembers = signal([], ...(ngDevMode ? [{ debugName: "groupMembers" }] : []));
3062
3118
  pendingInvitations = signal([], ...(ngDevMode ? [{ debugName: "pendingInvitations" }] : []));
3119
+ myPendingInvitations = signal([], ...(ngDevMode ? [{ debugName: "myPendingInvitations" }] : []));
3063
3120
  // UI state
3064
3121
  showCreateForm = signal(false, ...(ngDevMode ? [{ debugName: "showCreateForm" }] : []));
3065
3122
  showInviteForm = signal(false, ...(ngDevMode ? [{ debugName: "showInviteForm" }] : []));
@@ -3071,6 +3128,7 @@ class LabGroupsComponent {
3071
3128
  directMembersOnly = signal(false, ...(ngDevMode ? [{ debugName: "directMembersOnly" }] : []));
3072
3129
  // Computed values
3073
3130
  hasLabGroups = computed(() => this.labGroupsData().results.length > 0, ...(ngDevMode ? [{ debugName: "hasLabGroups" }] : []));
3131
+ hasMyPendingInvitations = computed(() => this.myPendingInvitations().length > 0, ...(ngDevMode ? [{ debugName: "hasMyPendingInvitations" }] : []));
3074
3132
  showPagination = computed(() => this.labGroupsData().count > this.pageSize(), ...(ngDevMode ? [{ debugName: "showPagination" }] : []));
3075
3133
  totalPages = computed(() => Math.ceil(this.labGroupsData().count / this.pageSize()), ...(ngDevMode ? [{ debugName: "totalPages" }] : []));
3076
3134
  hasSearchValue = computed(() => (this.searchForm?.get('search')?.value || '').trim().length > 0, ...(ngDevMode ? [{ debugName: "hasSearchValue" }] : []));
@@ -3106,6 +3164,7 @@ class LabGroupsComponent {
3106
3164
  ngOnInit() {
3107
3165
  this.setupSearch();
3108
3166
  this.loadInitialData();
3167
+ this.loadMyPendingInvitations();
3109
3168
  }
3110
3169
  loadInitialData() {
3111
3170
  this.searchParams.set({
@@ -3114,6 +3173,49 @@ class LabGroupsComponent {
3114
3173
  offset: 0
3115
3174
  });
3116
3175
  }
3176
+ loadMyPendingInvitations() {
3177
+ this.labGroupService.getMyPendingInvitations().subscribe({
3178
+ next: (invitations) => {
3179
+ this.myPendingInvitations.set(invitations);
3180
+ },
3181
+ error: (error) => {
3182
+ console.error('Error loading my invitations:', error);
3183
+ }
3184
+ });
3185
+ }
3186
+ acceptMyInvitation(invitationId) {
3187
+ this.isLoading.set(true);
3188
+ this.labGroupService.acceptLabGroupInvitation(invitationId).subscribe({
3189
+ next: (response) => {
3190
+ this.toastService.success(response.message);
3191
+ this.loadMyPendingInvitations();
3192
+ this.refreshLabGroups();
3193
+ this.isLoading.set(false);
3194
+ },
3195
+ error: (error) => {
3196
+ this.isLoading.set(false);
3197
+ const errorMsg = error?.error?.detail || error?.message || 'Failed to accept invitation.';
3198
+ this.toastService.error(errorMsg);
3199
+ }
3200
+ });
3201
+ }
3202
+ rejectMyInvitation(invitationId) {
3203
+ if (confirm('Are you sure you want to reject this invitation?')) {
3204
+ this.isLoading.set(true);
3205
+ this.labGroupService.rejectLabGroupInvitation(invitationId).subscribe({
3206
+ next: (response) => {
3207
+ this.toastService.success(response.message);
3208
+ this.loadMyPendingInvitations();
3209
+ this.isLoading.set(false);
3210
+ },
3211
+ error: (error) => {
3212
+ this.isLoading.set(false);
3213
+ const errorMsg = error?.error?.detail || error?.message || 'Failed to reject invitation.';
3214
+ this.toastService.error(errorMsg);
3215
+ }
3216
+ });
3217
+ }
3218
+ }
3117
3219
  setupSearch() {
3118
3220
  this.searchForm.valueChanges.pipe(debounceTime(300), distinctUntilChanged()).subscribe(formValue => {
3119
3221
  this.currentPage.set(1);
@@ -3348,12 +3450,12 @@ class LabGroupsComponent {
3348
3450
  this.loadGroupMembers(group.id);
3349
3451
  }
3350
3452
  }
3351
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LabGroupsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3352
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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 <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\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 @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\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" }] });
3453
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LabGroupsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3454
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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 <!-- My Pending Invitations -->\n @if (hasMyPendingInvitations()) {\n <div class=\"card mb-4 border-primary shadow-sm\">\n <div class=\"card-header bg-primary text-white\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-envelope-paper me-2\"></i>Lab Group Invitations\n <span class=\"badge bg-white text-primary ms-2\">{{ myPendingInvitations().length }}</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of myPendingInvitations(); track invitation.id) {\n <div class=\"list-group-item p-3\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <h6 class=\"mb-1\">You've been invited to join <strong>{{ invitation.labGroupName }}</strong></h6>\n <p class=\"mb-1 text-muted small\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'mediumDate' }}\n </p>\n @if (invitation.message) {\n <div class=\"alert alert-info py-2 px-3 mt-2 mb-0 small\">\n <i class=\"bi bi-chat-left-quote me-2\"></i>{{ invitation.message }}\n </div>\n }\n </div>\n <div class=\"d-flex gap-2\">\n <button \n type=\"button\" \n class=\"btn btn-success btn-sm px-3\"\n (click)=\"acceptMyInvitation(invitation.id)\">\n <i class=\"bi bi-check-circle me-1\"></i>Accept\n </button>\n <button \n type=\"button\" \n class=\"btn btn-outline-danger btn-sm\"\n (click)=\"rejectMyInvitation(invitation.id)\">\n <i class=\"bi bi-x-circle me-1\"></i>Reject\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n }\n\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 <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\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 @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\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" }] });
3353
3455
  }
3354
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: LabGroupsComponent, decorators: [{
3456
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: LabGroupsComponent, decorators: [{
3355
3457
  type: Component,
3356
- 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 <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\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 @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\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"] }]
3458
+ 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 <!-- My Pending Invitations -->\n @if (hasMyPendingInvitations()) {\n <div class=\"card mb-4 border-primary shadow-sm\">\n <div class=\"card-header bg-primary text-white\">\n <h5 class=\"mb-0\">\n <i class=\"bi bi-envelope-paper me-2\"></i>Lab Group Invitations\n <span class=\"badge bg-white text-primary ms-2\">{{ myPendingInvitations().length }}</span>\n </h5>\n </div>\n <div class=\"card-body p-0\">\n <div class=\"list-group list-group-flush\">\n @for (invitation of myPendingInvitations(); track invitation.id) {\n <div class=\"list-group-item p-3\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <h6 class=\"mb-1\">You've been invited to join <strong>{{ invitation.labGroupName }}</strong></h6>\n <p class=\"mb-1 text-muted small\">\n Invited by {{ invitation.inviterName }} on {{ invitation.createdAt | date:'mediumDate' }}\n </p>\n @if (invitation.message) {\n <div class=\"alert alert-info py-2 px-3 mt-2 mb-0 small\">\n <i class=\"bi bi-chat-left-quote me-2\"></i>{{ invitation.message }}\n </div>\n }\n </div>\n <div class=\"d-flex gap-2\">\n <button \n type=\"button\" \n class=\"btn btn-success btn-sm px-3\"\n (click)=\"acceptMyInvitation(invitation.id)\">\n <i class=\"bi bi-check-circle me-1\"></i>Accept\n </button>\n <button \n type=\"button\" \n class=\"btn btn-outline-danger btn-sm\"\n (click)=\"rejectMyInvitation(invitation.id)\">\n <i class=\"bi bi-x-circle me-1\"></i>Reject\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n }\n\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 <div class=\"d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0\">\n <i class=\"bi bi-people me-2\"></i>Current Members ({{ groupMembersCount() }})\n </h6>\n @if (hasSubGroups()) {\n <div class=\"form-check form-switch\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n id=\"directMembersOnly\"\n [checked]=\"directMembersOnly()\"\n (change)=\"toggleDirectMembersOnly()\">\n <label class=\"form-check-label small\" for=\"directMembersOnly\">\n Direct only\n </label>\n </div>\n }\n </div>\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 @if (showMemberPagination()) {\n <div class=\"card-footer\">\n <div class=\"d-flex justify-content-center\">\n <ngb-pagination\n [collectionSize]=\"memberTotal()\"\n [page]=\"memberPage()\"\n [pageSize]=\"memberPageSize()\"\n [maxSize]=\"5\"\n [boundaryLinks]=\"true\"\n [rotate]=\"true\"\n (pageChange)=\"onMemberPageChange($event)\"\n class=\"pagination-sm mb-0\">\n </ngb-pagination>\n </div>\n </div>\n }\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"] }]
3357
3459
  }], ctorParameters: () => [] });
3358
3460
 
3359
3461
  class UserProfileComponent {
@@ -3380,7 +3482,7 @@ class UserProfileComponent {
3380
3482
  // Computed signals for derived values
3381
3483
  fullName = computed(() => {
3382
3484
  const user = this.currentUser();
3383
- return user ? `${user.firstName} ${user.lastName}`.trim() : '';
3485
+ return this.userManagementService.getUserDisplayName(user);
3384
3486
  }, ...(ngDevMode ? [{ debugName: "fullName" }] : []));
3385
3487
  isStaff = computed(() => this.currentUser()?.isStaff || false, ...(ngDevMode ? [{ debugName: "isStaff" }] : []));
3386
3488
  joinDate = computed(() => {
@@ -3512,10 +3614,10 @@ class UserProfileComponent {
3512
3614
  }
3513
3615
  return null;
3514
3616
  }
3515
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3516
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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"] }] });
3617
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3618
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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"] }] });
3517
3619
  }
3518
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: UserProfileComponent, decorators: [{
3620
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: UserProfileComponent, decorators: [{
3519
3621
  type: Component,
3520
3622
  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>" }]
3521
3623
  }], ctorParameters: () => [] });
@@ -3728,10 +3830,10 @@ class SiteConfigComponent {
3728
3830
  fileInput.value = '';
3729
3831
  }
3730
3832
  }
3731
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: SiteConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3732
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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=\"d-flex gap-2 align-items-center\">\r\n <input\r\n type=\"color\"\r\n id=\"primary_color\"\r\n class=\"form-control form-control-color\"\r\n formControlName=\"primaryColor\"\r\n style=\"width: 60px; height: 40px;\">\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n formControlName=\"primaryColor\"\r\n placeholder=\"#1976d2\"\r\n [class.is-invalid]=\"configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched\">\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 <!-- Transcription Configuration Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-mic me-2\"></i>Audio/Video Transcription\r\n </h6>\r\n\r\n <!-- Whisper Model Selection -->\r\n <div class=\"mb-3\">\r\n <label for=\"whisper_model\" class=\"form-label\">\r\n <i class=\"bi bi-cpu me-1\"></i>\r\n Default Whisper.cpp Model\r\n </label>\r\n <div class=\"input-group\">\r\n <select\r\n class=\"form-select\"\r\n id=\"whisper_model\"\r\n formControlName=\"whisperCppModel\">\r\n @if (loadingModels()) {\r\n <option disabled selected>Loading available models...</option>\r\n } @else {\r\n @for (model of availableModels(); track model.path) {\r\n <option [value]=\"model.path\">\r\n {{ model.name }} ({{ model.size }}) - {{ model.description }}\r\n </option>\r\n }\r\n @if (availableModels().length === 0) {\r\n <option disabled selected>No models found. Click refresh to scan.</option>\r\n }\r\n }\r\n </select>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"refreshAvailableModels()\"\r\n [disabled]=\"refreshingModels() || loadingModels()\">\r\n @if (refreshingModels()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Scan Models\r\n </button>\r\n </div>\r\n <div class=\"form-text\">\r\n Model used for audio/video transcription. Larger models are more accurate but slower.\r\n The transcribe worker scans its filesystem to detect available models.\r\n </div>\r\n @if (availableModels().length > 0) {\r\n <div class=\"mt-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Found {{ availableModels().length }} model(s). Last scanned by transcribe worker on startup.\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n </form>\r\n\r\n <!-- Worker Status Section (Superuser Only) -->\r\n @if (isSuperuser()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header d-flex justify-content-between align-items-center\">\r\n <h6 class=\"mb-0\">\r\n <i class=\"bi bi-hdd-network me-2\"></i>Worker Status\r\n </h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\"\r\n (click)=\"loadWorkerStatus()\"\r\n [disabled]=\"loadingWorkerStatus()\">\r\n @if (loadingWorkerStatus()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Refresh\r\n </button>\r\n </div>\r\n <div class=\"card-body\">\r\n @if (loadingWorkerStatus()) {\r\n <div class=\"text-center py-3\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n } @else if (workerStatus()) {\r\n <!-- Workers -->\r\n <div class=\"mb-3\">\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-server me-1\"></i>\r\n Workers ({{ workerStatus().workerCount }})\r\n </h6>\r\n @if (workerStatus().workers.length === 0) {\r\n <div class=\"alert alert-warning mb-0\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n No workers found!\r\n </div>\r\n } @else {\r\n @for (worker of workerStatus().workers; track worker.name) {\r\n <div class=\"card mb-2\">\r\n <div class=\"card-body p-2\">\r\n <div class=\"d-flex justify-content-between align-items-start\">\r\n <div class=\"flex-grow-1\">\r\n <div class=\"d-flex align-items-center mb-1\">\r\n @if (worker.state === 'idle') {\r\n <span class=\"badge bg-success me-2\">Idle</span>\r\n } @else if (worker.state === 'busy') {\r\n <span class=\"badge bg-warning me-2\">Busy</span>\r\n } @else {\r\n <span class=\"badge bg-danger me-2\">{{ worker.state }}</span>\r\n }\r\n <small class=\"text-muted\">{{ worker.hostname }} (PID: {{ worker.pid }})</small>\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-list-ul me-1\"></i>\r\n Queues: {{ worker.queues.join(', ') }}\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-check-circle me-1\"></i>\r\n {{ worker.successfulJobCount }} successful, {{ worker.failedJobCount }} failed\r\n </div>\r\n @if (worker.currentJob) {\r\n <div class=\"small mt-1\">\r\n <i class=\"bi bi-hourglass-split me-1\"></i>\r\n <strong>Current:</strong> {{ worker.currentJob.funcName }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Queues -->\r\n <div>\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-list-task me-1\"></i>\r\n Queue Statistics\r\n </h6>\r\n <div class=\"table-responsive\">\r\n <table class=\"table table-sm mb-0\">\r\n <thead>\r\n <tr>\r\n <th>Queue</th>\r\n <th>Queued</th>\r\n <th>Started</th>\r\n <th>Failed</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (queue of Object.keys(workerStatus().queues); track queue) {\r\n <tr>\r\n <td>\r\n <span class=\"badge bg-secondary\">{{ queue }}</span>\r\n </td>\r\n <td>{{ workerStatus().queues[queue].count }}</td>\r\n <td>{{ workerStatus().queues[queue].startedCount }}</td>\r\n <td>\r\n @if (workerStatus().queues[queue].failedCount > 0) {\r\n <span class=\"text-danger\">{{ workerStatus().queues[queue].failedCount }}</span>\r\n } @else {\r\n {{ workerStatus().queues[queue].failedCount }}\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"text-center py-3\">\r\n <p class=\"text-muted mb-2\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Click refresh to load worker status\r\n </p>\r\n <small class=\"text-muted\">Only available to superusers</small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\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: [""], 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.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"] }] });
3833
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SiteConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3834
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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=\"d-flex gap-2 align-items-center\">\r\n <input\r\n type=\"color\"\r\n id=\"primary_color\"\r\n class=\"form-control form-control-color\"\r\n formControlName=\"primaryColor\"\r\n style=\"width: 60px; height: 40px;\">\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n formControlName=\"primaryColor\"\r\n placeholder=\"#1976d2\"\r\n [class.is-invalid]=\"configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched\">\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 <!-- Transcription Configuration Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-mic me-2\"></i>Audio/Video Transcription\r\n </h6>\r\n\r\n <!-- Whisper Model Selection -->\r\n <div class=\"mb-3\">\r\n <label for=\"whisper_model\" class=\"form-label\">\r\n <i class=\"bi bi-cpu me-1\"></i>\r\n Default Whisper.cpp Model\r\n </label>\r\n <div class=\"input-group\">\r\n <select\r\n class=\"form-select\"\r\n id=\"whisper_model\"\r\n formControlName=\"whisperCppModel\">\r\n @if (loadingModels()) {\r\n <option disabled selected>Loading available models...</option>\r\n } @else {\r\n @for (model of availableModels(); track model.path) {\r\n <option [value]=\"model.path\">\r\n {{ model.name }} ({{ model.size }}) - {{ model.description }}\r\n </option>\r\n }\r\n @if (availableModels().length === 0) {\r\n <option disabled selected>No models found. Click refresh to scan.</option>\r\n }\r\n }\r\n </select>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"refreshAvailableModels()\"\r\n [disabled]=\"refreshingModels() || loadingModels()\">\r\n @if (refreshingModels()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Scan Models\r\n </button>\r\n </div>\r\n <div class=\"form-text\">\r\n Model used for audio/video transcription. Larger models are more accurate but slower.\r\n The transcribe worker scans its filesystem to detect available models.\r\n </div>\r\n @if (availableModels().length > 0) {\r\n <div class=\"mt-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Found {{ availableModels().length }} model(s). Last scanned by transcribe worker on startup.\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n </form>\r\n\r\n <!-- Worker Status Section (Superuser Only) -->\r\n @if (isSuperuser()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header d-flex justify-content-between align-items-center\">\r\n <h6 class=\"mb-0\">\r\n <i class=\"bi bi-hdd-network me-2\"></i>Worker Status\r\n </h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\"\r\n (click)=\"loadWorkerStatus()\"\r\n [disabled]=\"loadingWorkerStatus()\">\r\n @if (loadingWorkerStatus()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Refresh\r\n </button>\r\n </div>\r\n <div class=\"card-body\">\r\n @if (loadingWorkerStatus()) {\r\n <div class=\"text-center py-3\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n } @else if (workerStatus()) {\r\n <!-- Workers -->\r\n <div class=\"mb-3\">\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-server me-1\"></i>\r\n Workers ({{ workerStatus().workerCount }})\r\n </h6>\r\n @if (workerStatus().workers.length === 0) {\r\n <div class=\"alert alert-warning mb-0\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n No workers found!\r\n </div>\r\n } @else {\r\n @for (worker of workerStatus().workers; track worker.name) {\r\n <div class=\"card mb-2\">\r\n <div class=\"card-body p-2\">\r\n <div class=\"d-flex justify-content-between align-items-start\">\r\n <div class=\"flex-grow-1\">\r\n <div class=\"d-flex align-items-center mb-1\">\r\n @if (worker.state === 'idle') {\r\n <span class=\"badge bg-success me-2\">Idle</span>\r\n } @else if (worker.state === 'busy') {\r\n <span class=\"badge bg-warning me-2\">Busy</span>\r\n } @else {\r\n <span class=\"badge bg-danger me-2\">{{ worker.state }}</span>\r\n }\r\n <small class=\"text-muted\">{{ worker.hostname }} (PID: {{ worker.pid }})</small>\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-list-ul me-1\"></i>\r\n Queues: {{ worker.queues.join(', ') }}\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-check-circle me-1\"></i>\r\n {{ worker.successfulJobCount }} successful, {{ worker.failedJobCount }} failed\r\n </div>\r\n @if (worker.currentJob) {\r\n <div class=\"small mt-1\">\r\n <i class=\"bi bi-hourglass-split me-1\"></i>\r\n <strong>Current:</strong> {{ worker.currentJob.funcName }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Queues -->\r\n <div>\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-list-task me-1\"></i>\r\n Queue Statistics\r\n </h6>\r\n <div class=\"table-responsive\">\r\n <table class=\"table table-sm mb-0\">\r\n <thead>\r\n <tr>\r\n <th>Queue</th>\r\n <th>Queued</th>\r\n <th>Started</th>\r\n <th>Failed</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (queue of Object.keys(workerStatus().queues); track queue) {\r\n <tr>\r\n <td>\r\n <span class=\"badge bg-secondary\">{{ queue }}</span>\r\n </td>\r\n <td>{{ workerStatus().queues[queue].count }}</td>\r\n <td>{{ workerStatus().queues[queue].startedCount }}</td>\r\n <td>\r\n @if (workerStatus().queues[queue].failedCount > 0) {\r\n <span class=\"text-danger\">{{ workerStatus().queues[queue].failedCount }}</span>\r\n } @else {\r\n {{ workerStatus().queues[queue].failedCount }}\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"text-center py-3\">\r\n <p class=\"text-muted mb-2\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Click refresh to load worker status\r\n </p>\r\n <small class=\"text-muted\">Only available to superusers</small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\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: [""], 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.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"] }] });
3733
3835
  }
3734
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: SiteConfigComponent, decorators: [{
3836
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SiteConfigComponent, decorators: [{
3735
3837
  type: Component,
3736
3838
  args: [{ selector: 'ccc-site-config', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert], 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=\"d-flex gap-2 align-items-center\">\r\n <input\r\n type=\"color\"\r\n id=\"primary_color\"\r\n class=\"form-control form-control-color\"\r\n formControlName=\"primaryColor\"\r\n style=\"width: 60px; height: 40px;\">\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n formControlName=\"primaryColor\"\r\n placeholder=\"#1976d2\"\r\n [class.is-invalid]=\"configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched\">\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 <!-- Transcription Configuration Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-mic me-2\"></i>Audio/Video Transcription\r\n </h6>\r\n\r\n <!-- Whisper Model Selection -->\r\n <div class=\"mb-3\">\r\n <label for=\"whisper_model\" class=\"form-label\">\r\n <i class=\"bi bi-cpu me-1\"></i>\r\n Default Whisper.cpp Model\r\n </label>\r\n <div class=\"input-group\">\r\n <select\r\n class=\"form-select\"\r\n id=\"whisper_model\"\r\n formControlName=\"whisperCppModel\">\r\n @if (loadingModels()) {\r\n <option disabled selected>Loading available models...</option>\r\n } @else {\r\n @for (model of availableModels(); track model.path) {\r\n <option [value]=\"model.path\">\r\n {{ model.name }} ({{ model.size }}) - {{ model.description }}\r\n </option>\r\n }\r\n @if (availableModels().length === 0) {\r\n <option disabled selected>No models found. Click refresh to scan.</option>\r\n }\r\n }\r\n </select>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"refreshAvailableModels()\"\r\n [disabled]=\"refreshingModels() || loadingModels()\">\r\n @if (refreshingModels()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Scan Models\r\n </button>\r\n </div>\r\n <div class=\"form-text\">\r\n Model used for audio/video transcription. Larger models are more accurate but slower.\r\n The transcribe worker scans its filesystem to detect available models.\r\n </div>\r\n @if (availableModels().length > 0) {\r\n <div class=\"mt-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Found {{ availableModels().length }} model(s). Last scanned by transcribe worker on startup.\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n </form>\r\n\r\n <!-- Worker Status Section (Superuser Only) -->\r\n @if (isSuperuser()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header d-flex justify-content-between align-items-center\">\r\n <h6 class=\"mb-0\">\r\n <i class=\"bi bi-hdd-network me-2\"></i>Worker Status\r\n </h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\"\r\n (click)=\"loadWorkerStatus()\"\r\n [disabled]=\"loadingWorkerStatus()\">\r\n @if (loadingWorkerStatus()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Refresh\r\n </button>\r\n </div>\r\n <div class=\"card-body\">\r\n @if (loadingWorkerStatus()) {\r\n <div class=\"text-center py-3\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n } @else if (workerStatus()) {\r\n <!-- Workers -->\r\n <div class=\"mb-3\">\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-server me-1\"></i>\r\n Workers ({{ workerStatus().workerCount }})\r\n </h6>\r\n @if (workerStatus().workers.length === 0) {\r\n <div class=\"alert alert-warning mb-0\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n No workers found!\r\n </div>\r\n } @else {\r\n @for (worker of workerStatus().workers; track worker.name) {\r\n <div class=\"card mb-2\">\r\n <div class=\"card-body p-2\">\r\n <div class=\"d-flex justify-content-between align-items-start\">\r\n <div class=\"flex-grow-1\">\r\n <div class=\"d-flex align-items-center mb-1\">\r\n @if (worker.state === 'idle') {\r\n <span class=\"badge bg-success me-2\">Idle</span>\r\n } @else if (worker.state === 'busy') {\r\n <span class=\"badge bg-warning me-2\">Busy</span>\r\n } @else {\r\n <span class=\"badge bg-danger me-2\">{{ worker.state }}</span>\r\n }\r\n <small class=\"text-muted\">{{ worker.hostname }} (PID: {{ worker.pid }})</small>\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-list-ul me-1\"></i>\r\n Queues: {{ worker.queues.join(', ') }}\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-check-circle me-1\"></i>\r\n {{ worker.successfulJobCount }} successful, {{ worker.failedJobCount }} failed\r\n </div>\r\n @if (worker.currentJob) {\r\n <div class=\"small mt-1\">\r\n <i class=\"bi bi-hourglass-split me-1\"></i>\r\n <strong>Current:</strong> {{ worker.currentJob.funcName }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Queues -->\r\n <div>\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-list-task me-1\"></i>\r\n Queue Statistics\r\n </h6>\r\n <div class=\"table-responsive\">\r\n <table class=\"table table-sm mb-0\">\r\n <thead>\r\n <tr>\r\n <th>Queue</th>\r\n <th>Queued</th>\r\n <th>Started</th>\r\n <th>Failed</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (queue of Object.keys(workerStatus().queues); track queue) {\r\n <tr>\r\n <td>\r\n <span class=\"badge bg-secondary\">{{ queue }}</span>\r\n </td>\r\n <td>{{ workerStatus().queues[queue].count }}</td>\r\n <td>{{ workerStatus().queues[queue].startedCount }}</td>\r\n <td>\r\n @if (workerStatus().queues[queue].failedCount > 0) {\r\n <span class=\"text-danger\">{{ workerStatus().queues[queue].failedCount }}</span>\r\n } @else {\r\n {{ workerStatus().queues[queue].failedCount }}\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"text-center py-3\">\r\n <p class=\"text-muted mb-2\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Click refresh to load worker status\r\n </p>\r\n <small class=\"text-muted\">Only available to superusers</small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\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>" }]
3737
3839
  }], ctorParameters: () => [] });
@@ -3802,10 +3904,10 @@ class DemoModeBannerComponent {
3802
3904
  const remaining = this.demoModeInfo.cleanupIntervalMinutes - elapsed;
3803
3905
  return Math.max(0, Math.floor(remaining));
3804
3906
  }
3805
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeBannerComponent, deps: [{ token: DemoModeService }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component });
3806
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.13", type: DemoModeBannerComponent, isStandalone: true, selector: "cupcake-demo-mode-banner", ngImport: i0, template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode && !isDismissed\" [class.collapsed]=\"isCollapsed\">\r\n <div class=\"banner-content\">\r\n <div class=\"banner-header\">\r\n <div class=\"banner-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\r\n </svg>\r\n </div>\r\n <div class=\"banner-title\">\r\n <strong>Demo Mode Active</strong>\r\n </div>\r\n <div class=\"banner-actions\">\r\n <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\" title=\"Collapse banner\">\r\n <svg *ngIf=\"!isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"6 9 12 15 18 9\"></polyline>\r\n </svg>\r\n <svg *ngIf=\"isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"18 15 12 9 6 15\"></polyline>\r\n </svg>\r\n </button>\r\n <button class=\"dismiss-btn\" (click)=\"dismissBanner()\" type=\"button\" title=\"Dismiss banner\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"banner-details\" *ngIf=\"!isCollapsed\">\r\n <p class=\"banner-message\">\r\n This is a demonstration environment. All data will be automatically reset every\r\n <strong>{{ demoModeInfo?.cleanupIntervalMinutes }} minutes</strong>.\r\n </p>\r\n <div class=\"banner-info\">\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <polyline points=\"12 6 12 12 16 14\"></polyline>\r\n </svg>\r\n Next reset: ~{{ getMinutesRemaining() }} minutes\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"></path>\r\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"></path>\r\n </svg>\r\n Transcription features are disabled\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"></rect>\r\n <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"></path>\r\n </svg>\r\n Read-only for non-demo users\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".demo-mode-banner{position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#ff6b35,#f7931e);color:#fff;box-shadow:0 2px 8px #0003;z-index:9999;transition:all .3s ease}.demo-mode-banner.collapsed .banner-details{display:none}.demo-mode-banner .banner-content{max-width:1200px;margin:0 auto;padding:12px 24px}.demo-mode-banner .banner-header{display:flex;align-items:center;gap:12px}.demo-mode-banner .banner-header .banner-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fff3;border-radius:50%}.demo-mode-banner .banner-header .banner-icon svg{width:20px;height:20px}.demo-mode-banner .banner-header .banner-title{flex:1;font-size:16px;line-height:1.5}.demo-mode-banner .banner-header .banner-title strong{font-weight:600}.demo-mode-banner .banner-header .banner-actions{display:flex;align-items:center;gap:4px}.demo-mode-banner .banner-header .collapse-btn,.demo-mode-banner .banner-header .dismiss-btn{background:transparent;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s ease}.demo-mode-banner .banner-header .collapse-btn:hover,.demo-mode-banner .banner-header .dismiss-btn:hover{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus,.demo-mode-banner .banner-header .dismiss-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.demo-mode-banner .banner-header .dismiss-btn:hover{background:#fff3}.demo-mode-banner .banner-details{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.2)}.demo-mode-banner .banner-details .banner-message{margin:0 0 12px;font-size:14px;line-height:1.6}.demo-mode-banner .banner-details .banner-message strong{font-weight:600;text-decoration:underline}.demo-mode-banner .banner-details .banner-info{display:flex;flex-wrap:wrap;gap:16px;font-size:13px}.demo-mode-banner .banner-details .banner-info .info-item{display:flex;align-items:center;gap:6px;background:#ffffff1a;padding:6px 12px;border-radius:4px}.demo-mode-banner .banner-details .banner-info .info-item svg{flex-shrink:0}@media (max-width: 768px){.demo-mode-banner .banner-content{padding:10px 16px}.demo-mode-banner .banner-header .banner-title{font-size:14px}.demo-mode-banner .banner-details .banner-info{flex-direction:column;gap:8px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
3907
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeBannerComponent, deps: [{ token: DemoModeService }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component });
3908
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: DemoModeBannerComponent, isStandalone: true, selector: "cupcake-demo-mode-banner", ngImport: i0, template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode && !isDismissed\" [class.collapsed]=\"isCollapsed\">\r\n <div class=\"banner-content\">\r\n <div class=\"banner-header\">\r\n <div class=\"banner-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\r\n </svg>\r\n </div>\r\n <div class=\"banner-title\">\r\n <strong>Demo Mode Active</strong>\r\n </div>\r\n <div class=\"banner-actions\">\r\n <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\" title=\"Collapse banner\">\r\n <svg *ngIf=\"!isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"6 9 12 15 18 9\"></polyline>\r\n </svg>\r\n <svg *ngIf=\"isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"18 15 12 9 6 15\"></polyline>\r\n </svg>\r\n </button>\r\n <button class=\"dismiss-btn\" (click)=\"dismissBanner()\" type=\"button\" title=\"Dismiss banner\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"banner-details\" *ngIf=\"!isCollapsed\">\r\n <p class=\"banner-message\">\r\n This is a demonstration environment. All data will be automatically reset every\r\n <strong>{{ demoModeInfo?.cleanupIntervalMinutes }} minutes</strong>.\r\n </p>\r\n <div class=\"banner-info\">\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <polyline points=\"12 6 12 12 16 14\"></polyline>\r\n </svg>\r\n Next reset: ~{{ getMinutesRemaining() }} minutes\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"></path>\r\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"></path>\r\n </svg>\r\n Transcription features are disabled\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"></rect>\r\n <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"></path>\r\n </svg>\r\n Read-only for non-demo users\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".demo-mode-banner{position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#ff6b35,#f7931e);color:#fff;box-shadow:0 2px 8px #0003;z-index:9999;transition:all .3s ease}.demo-mode-banner.collapsed .banner-details{display:none}.demo-mode-banner .banner-content{max-width:1200px;margin:0 auto;padding:12px 24px}.demo-mode-banner .banner-header{display:flex;align-items:center;gap:12px}.demo-mode-banner .banner-header .banner-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fff3;border-radius:50%}.demo-mode-banner .banner-header .banner-icon svg{width:20px;height:20px}.demo-mode-banner .banner-header .banner-title{flex:1;font-size:16px;line-height:1.5}.demo-mode-banner .banner-header .banner-title strong{font-weight:600}.demo-mode-banner .banner-header .banner-actions{display:flex;align-items:center;gap:4px}.demo-mode-banner .banner-header .collapse-btn,.demo-mode-banner .banner-header .dismiss-btn{background:transparent;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s ease}.demo-mode-banner .banner-header .collapse-btn:hover,.demo-mode-banner .banner-header .dismiss-btn:hover{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus,.demo-mode-banner .banner-header .dismiss-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.demo-mode-banner .banner-header .dismiss-btn:hover{background:#fff3}.demo-mode-banner .banner-details{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.2)}.demo-mode-banner .banner-details .banner-message{margin:0 0 12px;font-size:14px;line-height:1.6}.demo-mode-banner .banner-details .banner-message strong{font-weight:600;text-decoration:underline}.demo-mode-banner .banner-details .banner-info{display:flex;flex-wrap:wrap;gap:16px;font-size:13px}.demo-mode-banner .banner-details .banner-info .info-item{display:flex;align-items:center;gap:6px;background:#ffffff1a;padding:6px 12px;border-radius:4px}.demo-mode-banner .banner-details .banner-info .info-item svg{flex-shrink:0}@media (max-width: 768px){.demo-mode-banner .banner-content{padding:10px 16px}.demo-mode-banner .banner-header .banner-title{font-size:14px}.demo-mode-banner .banner-details .banner-info{flex-direction:column;gap:8px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
3807
3909
  }
3808
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: DemoModeBannerComponent, decorators: [{
3910
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DemoModeBannerComponent, decorators: [{
3809
3911
  type: Component,
3810
3912
  args: [{ selector: 'cupcake-demo-mode-banner', standalone: true, imports: [CommonModule], template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode && !isDismissed\" [class.collapsed]=\"isCollapsed\">\r\n <div class=\"banner-content\">\r\n <div class=\"banner-header\">\r\n <div class=\"banner-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\r\n </svg>\r\n </div>\r\n <div class=\"banner-title\">\r\n <strong>Demo Mode Active</strong>\r\n </div>\r\n <div class=\"banner-actions\">\r\n <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\" title=\"Collapse banner\">\r\n <svg *ngIf=\"!isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"6 9 12 15 18 9\"></polyline>\r\n </svg>\r\n <svg *ngIf=\"isCollapsed\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <polyline points=\"18 15 12 9 6 15\"></polyline>\r\n </svg>\r\n </button>\r\n <button class=\"dismiss-btn\" (click)=\"dismissBanner()\" type=\"button\" title=\"Dismiss banner\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"banner-details\" *ngIf=\"!isCollapsed\">\r\n <p class=\"banner-message\">\r\n This is a demonstration environment. All data will be automatically reset every\r\n <strong>{{ demoModeInfo?.cleanupIntervalMinutes }} minutes</strong>.\r\n </p>\r\n <div class=\"banner-info\">\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <polyline points=\"12 6 12 12 16 14\"></polyline>\r\n </svg>\r\n Next reset: ~{{ getMinutesRemaining() }} minutes\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"></path>\r\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"></path>\r\n </svg>\r\n Transcription features are disabled\r\n </div>\r\n <div class=\"info-item\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"></rect>\r\n <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"></path>\r\n </svg>\r\n Read-only for non-demo users\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".demo-mode-banner{position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#ff6b35,#f7931e);color:#fff;box-shadow:0 2px 8px #0003;z-index:9999;transition:all .3s ease}.demo-mode-banner.collapsed .banner-details{display:none}.demo-mode-banner .banner-content{max-width:1200px;margin:0 auto;padding:12px 24px}.demo-mode-banner .banner-header{display:flex;align-items:center;gap:12px}.demo-mode-banner .banner-header .banner-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fff3;border-radius:50%}.demo-mode-banner .banner-header .banner-icon svg{width:20px;height:20px}.demo-mode-banner .banner-header .banner-title{flex:1;font-size:16px;line-height:1.5}.demo-mode-banner .banner-header .banner-title strong{font-weight:600}.demo-mode-banner .banner-header .banner-actions{display:flex;align-items:center;gap:4px}.demo-mode-banner .banner-header .collapse-btn,.demo-mode-banner .banner-header .dismiss-btn{background:transparent;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s ease}.demo-mode-banner .banner-header .collapse-btn:hover,.demo-mode-banner .banner-header .dismiss-btn:hover{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus,.demo-mode-banner .banner-header .dismiss-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.demo-mode-banner .banner-header .dismiss-btn:hover{background:#fff3}.demo-mode-banner .banner-details{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.2)}.demo-mode-banner .banner-details .banner-message{margin:0 0 12px;font-size:14px;line-height:1.6}.demo-mode-banner .banner-details .banner-message strong{font-weight:600;text-decoration:underline}.demo-mode-banner .banner-details .banner-info{display:flex;flex-wrap:wrap;gap:16px;font-size:13px}.demo-mode-banner .banner-details .banner-info .info-item{display:flex;align-items:center;gap:6px;background:#ffffff1a;padding:6px 12px;border-radius:4px}.demo-mode-banner .banner-details .banner-info .info-item svg{flex-shrink:0}@media (max-width: 768px){.demo-mode-banner .banner-content{padding:10px 16px}.demo-mode-banner .banner-header .banner-title{font-size:14px}.demo-mode-banner .banner-details .banner-info{flex-direction:column;gap:8px}}\n"] }]
3811
3913
  }], ctorParameters: () => [{ type: DemoModeService }, { type: Document, decorators: [{
@@ -3852,10 +3954,10 @@ class ToastContainerComponent {
3852
3954
  return 'bi-info-circle-fill';
3853
3955
  }
3854
3956
  }
3855
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3856
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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" }] });
3957
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3958
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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" }] });
3857
3959
  }
3858
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: ToastContainerComponent, decorators: [{
3960
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ToastContainerComponent, decorators: [{
3859
3961
  type: Component,
3860
3962
  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"] }]
3861
3963
  }], ctorParameters: () => [] });
@@ -3863,10 +3965,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImpo
3863
3965
  class PoweredByFooterComponent {
3864
3966
  siteConfigService = inject(SiteConfigService);
3865
3967
  siteConfig$ = this.siteConfigService.config$;
3866
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: PoweredByFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3867
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.13", 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" }] });
3968
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PoweredByFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3969
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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" }] });
3868
3970
  }
3869
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: PoweredByFooterComponent, decorators: [{
3971
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PoweredByFooterComponent, decorators: [{
3870
3972
  type: Component,
3871
3973
  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"] }]
3872
3974
  }] });
@@ -3883,8 +3985,8 @@ class CupcakeCoreModule {
3883
3985
  ]
3884
3986
  };
3885
3987
  }
3886
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: CupcakeCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3887
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.13", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
3988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CupcakeCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3989
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.17", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
3888
3990
  ReactiveFormsModule,
3889
3991
  HttpClientModule,
3890
3992
  RouterModule,
@@ -3897,7 +3999,7 @@ class CupcakeCoreModule {
3897
3999
  CommonModule,
3898
4000
  ReactiveFormsModule,
3899
4001
  NgbModule] });
3900
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
4002
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CupcakeCoreModule, imports: [CommonModule,
3901
4003
  ReactiveFormsModule,
3902
4004
  HttpClientModule,
3903
4005
  RouterModule,
@@ -3908,7 +4010,7 @@ class CupcakeCoreModule {
3908
4010
  ReactiveFormsModule,
3909
4011
  NgbModule] });
3910
4012
  }
3911
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.13", ngImport: i0, type: CupcakeCoreModule, decorators: [{
4013
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CupcakeCoreModule, decorators: [{
3912
4014
  type: NgModule,
3913
4015
  args: [{
3914
4016
  declarations: [],